#include "extract_unified.h" #include "arena.h" // cbm_arena_sprintf #include "cbm.h" // CBMExtractCtx #include "helpers.h" #include "lang_specs.h" // CBMLangSpec, cbm_lang_spec, CBM_LANG_* #include "tree_sitter/api.h" // TSNode, TSTreeCursor, ts_tree_cursor_*, ts_node_* #include "foundation/constants.h" enum { MAX_INFRA_BINDINGS = 8 }; #include // uint32_t, uint8_t #include // --- Scope stack management --- static void push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) { if (state->scope_top >= MAX_SCOPES) { return; } state->scopes[state->scope_top].kind = kind; state->scopes[state->scope_top].depth = depth; state->scopes[state->scope_top].qn = qn; state->scope_top++; } // Pop scopes that we've ascended out of (depth >= current cursor depth). static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) { while (state->scope_top > 0 && state->scopes[state->scope_top - SKIP_ONE].depth >= cur_depth) { state->scope_top--; } } // Recompute state flags from the current scope stack. static void recompute_state(WalkState *state, const char *module_qn) { state->enclosing_func_qn = module_qn; state->enclosing_class_qn = NULL; state->inside_call = false; state->inside_import = false; state->loop_depth = 0; state->branch_depth = 0; for (int i = 0; i < state->scope_top; i++) { switch (state->scopes[i].kind) { case SCOPE_FUNC: state->enclosing_func_qn = state->scopes[i].qn; break; case SCOPE_CLASS: state->enclosing_class_qn = state->scopes[i].qn; break; case SCOPE_CALL: state->inside_call = true; break; case SCOPE_IMPORT: state->inside_import = true; break; case SCOPE_LOOP: state->loop_depth++; break; case SCOPE_BRANCH: state->branch_depth++; break; default: break; } } } // Try to resolve Wolfram function QN from set_delayed_top/set_top/set_delayed/set LHS. static const char *compute_wolfram_func_qn(CBMExtractCtx *ctx, TSNode node) { const char *nk = ts_node_type(node); if (strcmp(nk, "set_delayed_top") != 0 && strcmp(nk, "set_top") != 0 && strcmp(nk, "set_delayed") != 0 && strcmp(nk, "set") != 0) { return NULL; // not a Wolfram set node — signal caller to continue } if (ts_node_named_child_count(node) > 0) { TSNode lhs = ts_node_named_child(node, 0); if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) { TSNode head = ts_node_named_child(lhs, 0); if (strcmp(ts_node_type(head), "user_symbol") == 0) { char *name = cbm_node_text(ctx->arena, head, ctx->source); if (name && name[0]) { return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); } } } } return NULL; } /* True for a Lisp def-form head symbol (defn/define/...). Mirrors * lisp_is_def_head() in extract_defs.c so the scope-stack walk pushes a * SCOPE_FUNC only for actual definitions, never for a plain call list such as * `(add x 1)` — otherwise every parenthesized form would shadow the enclosing * def and the in-body call would mis-source. */ static bool lisp_head_is_def(const char *t) { if (!t) { return false; } static const char *heads[] = {"defn", "defn-", "def", "defmacro", "defmulti", "defmethod", "defprotocol", "defrecord", "deftype", "definterface", "defonce", "define", "define-syntax", "define-values", "define-syntax-rule", "define-struct", "define-record-type", "define/contract", "struct", NULL}; for (int i = 0; heads[i]; i++) { if (strcmp(t, heads[i]) == 0) { return true; } } return false; } /* Resolve a Lisp (Clojure/Scheme/Racket) def-form's QN for scope tracking. * The def node is a list/list_lit whose head names the def kind and whose * second element is the name (a bare symbol) or a (name args...) nested list. * Returns NULL for any non-def list (calls, vectors of args, the +/- body * forms, ...), so push_boundary_scopes pushes no scope for them. Mirrors * extract_lisp_def() in extract_defs.c. */ static const char *compute_lisp_func_qn(CBMExtractCtx *ctx, TSNode node) { if (ts_node_named_child_count(node) < 2) { return NULL; } char *head = cbm_node_text(ctx->arena, ts_node_named_child(node, 0), ctx->source); if (!lisp_head_is_def(head)) { return NULL; } 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 NULL; } char *name = cbm_node_text(ctx->arena, name_node, ctx->source); if (!name || !name[0]) { return NULL; } return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); } /* Resolve an Elixir def/defp/defmacro's QN for scope tracking. The def is a * `call` node whose target (first child) is the def macro and whose first * argument is either the function head call `name(args)` or a bare identifier * (zero-arg). Returns NULL for a non-def `call` (e.g. the in-body `add(x,1)` * call, whose target is not a def macro) so only defs push a scope. Mirrors * extract_elixir_func_def() in extract_defs.c. */ static const char *compute_elixir_func_qn(CBMExtractCtx *ctx, TSNode node) { if (ts_node_child_count(node) == 0) { return NULL; } char *macro = cbm_node_text(ctx->arena, ts_node_child(node, 0), ctx->source); if (!macro || (strcmp(macro, "def") != 0 && strcmp(macro, "defp") != 0 && strcmp(macro, "defmacro") != 0)) { return NULL; } TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); if (ts_node_is_null(args) && ts_node_child_count(node) > 1) { args = ts_node_child(node, 1); } if (ts_node_is_null(args) || ts_node_child_count(args) == 0) { return NULL; } TSNode first_arg = ts_node_child(args, 0); if (ts_node_is_null(first_arg)) { return NULL; } 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(ctx->arena, ts_node_child(first_arg, 0), ctx->source); } else if (strcmp(fk, "identifier") == 0) { name = cbm_node_text(ctx->arena, first_arg, ctx->source); } if (!name || !name[0]) { return NULL; } return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); } /* Resolve a CFML tag-function's QN for scope tracking. A * is a `cf_function_tag`; the name lives in a `cf_attribute` child (name="foo"), * not on a `name` field, so the shared resolver (which has no source pointer to * read the attribute NAME and disambiguate) cannot name it. The def-extractor * extract_cfml_function_tag() does the same attribute walk; this mirrors it so * the in-body call sources to the cffunction Function rather than the Module. */ static const char *compute_cfml_func_qn(CBMExtractCtx *ctx, TSNode node) { if (strcmp(ts_node_type(node), "cf_function_tag") != 0) { return NULL; } 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(ctx->arena, an, ctx->source); if (!aname || strcasecmp(aname, "name") != 0) { 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(ctx->arena, ts_node_is_null(inner) ? val : inner, ctx->source); } if (!name || !name[0]) { return NULL; } return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); } /* Resolve a Go-template named-template's QN for scope tracking. A * {{ define "greeting" }} ... {{ end }} is a `define_action` whose name is a * quoted `interpreted_string_literal` child, not a bare identifier on a `name` * field. The shared resolver can't strip the quotes (no source pointer), so the * gate lives here. Mirrors extract_gotemplate_define() so a {{ template }}/include * call inside the define body sources to the define's Function, not the Module. */ static const char *compute_gotemplate_func_qn(CBMExtractCtx *ctx, TSNode node) { if (strcmp(ts_node_type(node), "define_action") != 0) { return NULL; } TSNode s = cbm_find_child_by_kind(node, "interpreted_string_literal"); if (ts_node_is_null(s)) { return NULL; } char *raw = cbm_node_text(ctx->arena, s, ctx->source); if (!raw) { return NULL; } size_t len = strlen(raw); if (len >= 2 && (raw[0] == '"' || raw[0] == '`')) { raw = cbm_arena_strndup(ctx->arena, raw + 1, len - 2); // strip surrounding quotes } if (!raw || !raw[0]) { return NULL; } return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, raw); } // Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic). static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { (void)spec; if (ctx->language == CBM_LANG_WOLFRAM) { return compute_wolfram_func_qn(ctx, node); } /* CFML tag dialect: is a cf_function_tag whose name * lives in a cf_attribute, not a `name` field — gate here where ctx->source * is available to read the attribute. Other CFML func nodes (embedded * CFScript function_declaration/_expression) fall through to the shared * resolver below. */ if (ctx->language == CBM_LANG_CFML && strcmp(ts_node_type(node), "cf_function_tag") == 0) { return compute_cfml_func_qn(ctx, node); } /* Go templates: {{ define "x" }} is a define_action whose name is a quoted * string literal — strip the quotes here (the shared resolver has no source). */ if (ctx->language == CBM_LANG_GOTEMPLATE) { return compute_gotemplate_func_qn(ctx, node); } /* Lisp family (Clojure/Scheme/Racket): the def node is a list/list_lit, a * very general kind that also matches plain call forms. The shared resolver * has no source pointer to read the head symbol, so the def-vs-call gate * lives here (we have ctx->source). Non-def lists return NULL → no scope * pushed → the in-body call sources to the enclosing def, not the Module. */ if (ctx->language == CBM_LANG_CLOJURE || ctx->language == CBM_LANG_SCHEME || ctx->language == CBM_LANG_RACKET) { return compute_lisp_func_qn(ctx, node); } /* Elixir: def/defp/defmacro are `call` nodes (so is every in-body call). * Gate on the def-macro target text so only definitions push a scope. */ if (ctx->language == CBM_LANG_ELIXIR) { return compute_elixir_func_qn(ctx, node); } /* Objective-C: a method_definition's selector keyword is a plain `identifier` * child. Resolve the call-scope QN HERE (not via the shared cbm_resolve_func_name) * so an in-body call sources to the method — without making the shared resolver * report the method as a top-level Function (the @implementation class-member * pass already emits the Method node; a shared-resolver name would double it). */ if (ctx->language == CBM_LANG_OBJC && strcmp(ts_node_type(node), "method_definition") == 0) { TSNode id = cbm_find_child_by_kind(node, "identifier"); if (!ts_node_is_null(id)) { char *mname = cbm_node_text(ctx->arena, id, ctx->source); if (mname && mname[0]) { if (state->enclosing_class_qn) { return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, mname); } return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, mname, ctx->language); } } } /* Dart: function_signature / method_signature have no `name` field; the name * is an `identifier` child (method_signature wraps a function_signature). The * shared resolver doesn't cover them, so resolve here for call-scope so an * in-body call sources to the function, not the Module. */ if (ctx->language == CBM_LANG_DART && (strcmp(ts_node_type(node), "function_signature") == 0 || strcmp(ts_node_type(node), "method_signature") == 0)) { TSNode sig = node; if (strcmp(ts_node_type(node), "method_signature") == 0) { TSNode fs = cbm_find_child_by_kind(node, "function_signature"); if (!ts_node_is_null(fs)) { sig = fs; } } TSNode id = cbm_find_child_by_kind(sig, "identifier"); if (!ts_node_is_null(id)) { char *nm = cbm_node_text(ctx->arena, id, ctx->source); if (nm && nm[0]) { if (state->enclosing_class_qn) { return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, nm); } return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, nm, ctx->language); } } } /* Agda: a definition is two `function` nodes — the type signature * (`compute : Nat -> Nat`, lhs has a `function_name` child that names the * def) and the body clause (`compute x = add x 1`, lhs has no function_name). * The shared resolver deliberately returns NULL for the body clause to avoid * a duplicate def, so an in-body call would source to the Module. Resolve the * body clause's name here (call-scope only) from the lhs head identifier so * the call attributes to the function. */ if (ctx->language == CBM_LANG_AGDA && strcmp(ts_node_type(node), "function") == 0) { TSNode lhs = cbm_find_child_by_kind(node, "lhs"); if (!ts_node_is_null(lhs)) { TSNode nm = cbm_find_child_by_kind(lhs, "function_name"); if (ts_node_is_null(nm)) { /* Body clause: descend to the first leaf of the lhs (`compute x` * -> the head `compute`). */ TSNode cur = lhs; for (int hop = 0; hop < 8 && !ts_node_is_null(cur) && ts_node_named_child_count(cur) > 0; hop++) { cur = ts_node_named_child(cur, 0); } nm = cur; } if (!ts_node_is_null(nm)) { char *name = cbm_node_text(ctx->arena, nm, ctx->source); if (name && name[0]) { return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name, ctx->language); } } } } /* Resolve the function name via the single shared resolver (extract_defs) so * call-scope attribution agrees with definition extraction across all ~130 * grammars. The old private 4-case copy returned NULL for Fortran subroutine, * SCSS mixin, SQL create_function, Julia short-form, etc., so * push_boundary_scopes never pushed a SCOPE_FUNC and the calls inside were * mis-attributed to the enclosing Module (QUALITY_ANALYSIS gap #3). */ TSNode name_node = cbm_resolve_func_name(node, ctx->language); if (ts_node_is_null(name_node)) { return NULL; } char *name = cbm_func_name_node_text(ctx->arena, name_node, ctx->source); if (!name || !name[0]) { return NULL; } /* C++/CUDA out-of-line method `void Foo::bar() {...}`: the def extractor * records this as Method "proj.file.Foo.bar". The call-scope QN must match * (be class-qualified) so an in-body call sources to the method, not a bare * "proj.file.bar" that no node carries (#554/#621). The out-of-line def is at * file scope, so enclosing_class_qn is NULL — derive the class from the * qualified declarator instead. */ 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(ctx->arena, node, ctx->source); if (scope_name && scope_name[0]) { const char *class_qn = cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, scope_name); return cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, name); } } if (state->enclosing_class_qn) { return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, name); } /* Java/Go: directory-based module so this enclosing-func QN matches the def * QN and the LSP caller_qn (the lsp_resolve join keys on exact equality). */ return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name, ctx->language); } // Compute class QN for scope tracking. static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkState *state) { TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); /* Newer tree-sitter-kotlin: class/object name is a type_identifier child. */ if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_KOTLIN) { name_node = cbm_find_child_by_kind(node, "type_identifier"); } /* Objective-C: class_interface / class_implementation have no `name` field; * the class name is a plain `identifier` child. Without this the walk pushes * no class scope, so a method body's calls source to the Module and the * method itself is mis-extracted as a top-level Function (not a Method). */ if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) { name_node = cbm_find_child_by_kind(node, "identifier"); } /* Rust: impl_item has no `name` field; the implementing type is in the `type` * field (`impl Calc {...}` / `impl Trait for Calc {...}` both -> Calc). The * dedicated impl handler in push_boundary_scopes is dead code (impl_item is in * rust_class_types, so the class branch runs first and lands here), so resolve * the type here. Without a class scope, an impl method's QN drops the type * (proj.file.method) and no longer matches the class-qualified def-side Method * node, so in-body calls fall back to the Module. */ if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) { name_node = ts_node_child_by_field_name(node, TS_FIELD("type")); } if (ts_node_is_null(name_node)) { return NULL; } char *name = cbm_node_text(ctx->arena, name_node, ctx->source); if (!name || !name[0]) { return NULL; } /* Nested class: prefix with the enclosing class QN (Outer.Inner) so this * scope QN matches the def-side class QN (extract_defs.c compute_class_qn / * extract_class_def), which the lsp_resolve join requires for nested types. */ if (state && state->enclosing_class_qn) { return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, name); } /* Java/Go: directory-based module (see compute_func_qn). */ return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name, ctx->language); } /* Forward declaration */ static bool is_string_node(const char *kind); // --- Module-level constant collection --- static void handle_string_constants(CBMExtractCtx *ctx, TSNode node, const WalkState *state) { /* Only collect at module level (not inside functions/classes) */ if (state->enclosing_func_qn != NULL && state->enclosing_func_qn != ctx->module_qn) { return; } const char *kind = ts_node_type(node); /* Python: expression_statement → assignment → identifier = string */ /* Go: short_var_declaration, const_spec */ /* JS/TS: variable_declarator, lexical_declaration */ if (strcmp(kind, "assignment") != 0 && strcmp(kind, "expression_statement") != 0 && strcmp(kind, "short_var_declaration") != 0 && strcmp(kind, "const_spec") != 0 && strcmp(kind, "variable_declarator") != 0) { return; } /* Find name (left side) and value (right side) */ TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("left")); TSNode value_node = ts_node_child_by_field_name(node, TS_FIELD("right")); /* Some grammars use "name" + "value" fields */ if (ts_node_is_null(name_node)) { name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); } if (ts_node_is_null(value_node)) { value_node = ts_node_child_by_field_name(node, TS_FIELD("value")); } if (ts_node_is_null(name_node) || ts_node_is_null(value_node)) { return; } /* Name must be an identifier */ const char *name_kind = ts_node_type(name_node); if (strcmp(name_kind, "identifier") != 0 && strcmp(name_kind, "constant") != 0) { return; } /* Value must be a string literal */ if (!is_string_node(ts_node_type(value_node))) { return; } char *name = cbm_node_text(ctx->arena, name_node, ctx->source); char *value = cbm_node_text(ctx->arena, value_node, ctx->source); if (!name || !name[0] || !value || !value[0]) { return; } /* Strip quotes from value */ int vlen = (int)strlen(value); if (vlen >= CBM_QUOTE_PAIR && (value[0] == '"' || value[0] == '\'')) { value = cbm_arena_strndup(ctx->arena, value + SKIP_ONE, (size_t)(vlen - PAIR_LEN)); if (!value) { return; } } /* Add to constant map */ CBMStringConstantMap *map = &ctx->string_constants; if (map->count < CBM_MAX_STRING_CONSTANTS) { map->names[map->count] = name; map->values[map->count] = value; map->count++; } } // --- String literal collection --- static bool is_string_node(const char *kind) { /* Common string literal node types across tree-sitter grammars */ return (strcmp(kind, "string_literal") == 0 || strcmp(kind, "string") == 0 || strcmp(kind, "string_content") == 0 || strcmp(kind, "interpreted_string_literal") == 0 || strcmp(kind, "raw_string_literal") == 0 || strcmp(kind, "string_value") == 0 || /* YAML string types */ strcmp(kind, "double_quote_scalar") == 0 || strcmp(kind, "single_quote_scalar") == 0); } static void handle_string_refs(CBMExtractCtx *ctx, TSNode node, const WalkState *state) { const char *kind = ts_node_type(node); if (!is_string_node(kind)) { return; } /* Extract string content */ char *text = cbm_node_text(ctx->arena, node, ctx->source); if (!text || !text[0]) { return; } /* Strip quotes if present */ int len = (int)strlen(text); const char *content = text; if (len >= CBM_QUOTE_PAIR && (text[0] == '"' || text[0] == '\'')) { content = text + SKIP_ONE; len -= PAIR_LEN; if (len <= 0) { return; } } /* Classify */ int kind_val = cbm_classify_string(content, len); if (kind_val < 0) { return; } /* Build null-terminated content string in arena */ char *val = cbm_arena_strndup(ctx->arena, content, (size_t)len); if (!val) { return; } CBMStringRef ref = { .value = val, .enclosing_func_qn = state->enclosing_func_qn ? state->enclosing_func_qn : ctx->module_qn, .kind = (CBMStringRefKind)kind_val, }; cbm_stringref_push(&ctx->result->string_refs, ctx->arena, ref); } // --- YAML nested field extraction (D2) --- /* Recursively walk YAML block_mapping_pair nodes, building dotted key paths. * Emits string_refs with key_path for leaf values that are URLs or config values. * Example: body.operational_info.post_url → "https://..." */ // Classify and emit a YAML leaf value as a string_ref with key_path. static void emit_yaml_leaf_value(CBMExtractCtx *ctx, TSNode val, const char *path) { char *val_text = cbm_node_text(ctx->arena, val, ctx->source); if (!val_text || !val_text[0]) { return; } int vlen = (int)strlen(val_text); const char *content = val_text; if (vlen >= CBM_QUOTE_PAIR && (val_text[0] == '"' || val_text[0] == '\'')) { content = val_text + SKIP_ONE; vlen -= PAIR_LEN; if (vlen <= 0) { return; } } int kind_val = cbm_classify_string(content, vlen); if (kind_val < 0) { return; } char *stored = cbm_arena_strndup(ctx->arena, content, (size_t)vlen); if (!stored) { return; } CBMStringRef ref = { .value = stored, .enclosing_func_qn = ctx->module_qn, .key_path = path, .kind = (CBMStringRefKind)kind_val, }; cbm_stringref_push(&ctx->result->string_refs, ctx->arena, ref); } typedef struct { TSNode node; const char *prefix; } yaml_walk_frame_t; #define YAML_WALK_STACK_CAP CBM_SZ_256 /* Push block_mapping children of a block_node/block_mapping value onto the walk stack. */ static void push_yaml_block_children(TSNode val, const char *path, yaml_walk_frame_t *stack, int *top) { uint32_t vnc = ts_node_named_child_count(val); for (int vi = (int)vnc - SKIP_ONE; vi >= 0 && *top < YAML_WALK_STACK_CAP; vi--) { TSNode vc = ts_node_named_child(val, (uint32_t)vi); const char *vctype = ts_node_type(vc); if (strcmp(vctype, "block_mapping") == 0 || strcmp(vctype, "block_mapping_pair") == 0) { stack[(*top)++] = (yaml_walk_frame_t){vc, path}; } } } static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode root, const char *root_prefix) { yaml_walk_frame_t stack[YAML_WALK_STACK_CAP]; int top = 0; stack[top++] = (yaml_walk_frame_t){root, root_prefix}; while (top > 0) { yaml_walk_frame_t frame = stack[--top]; TSNode node = frame.node; const char *prefix = frame.prefix; uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode child = ts_node_named_child(node, i); if (strcmp(ts_node_type(child), "block_mapping_pair") != 0) { continue; } TSNode key = ts_node_child_by_field_name(child, TS_FIELD("key")); if (ts_node_is_null(key)) { continue; } char *key_text = cbm_node_text(ctx->arena, key, ctx->source); if (!key_text || !key_text[0]) { continue; } const char *path = prefix ? cbm_arena_sprintf(ctx->arena, "%s.%s", prefix, key_text) : key_text; TSNode val = ts_node_child_by_field_name(child, TS_FIELD("value")); if (ts_node_is_null(val)) { continue; } const char *vk = ts_node_type(val); if (strcmp(vk, "block_node") == 0 || strcmp(vk, "block_mapping") == 0) { push_yaml_block_children(val, path, stack, &top); continue; } emit_yaml_leaf_value(ctx, val, path); } } } /* ── Infrastructure binding extraction ───────────────────────────── * Scan YAML/JSON/HCL list items for topic→URL pairs. * Patterns detected: * YAML: {topic: X, config: {push_endpoint: URL}} (Pub/Sub subscription) * YAML: {uri: URL, body: ...} (Cloud Scheduler) * YAML: {queue: X, uri: URL} (Cloud Tasks) * HCL: resource "google_pubsub_subscription" { topic=X, push_config{push_endpoint=URL} } * * Works by collecting key-value pairs in each mapping, then checking for * known source+target patterns. Language-agnostic: the key names are the signal. */ /* Source key names (topic/queue/schedule identifier) */ static int is_source_key(const char *key) { return (strcmp(key, "topic") == 0 || strcmp(key, "queue") == 0 || strcmp(key, "queue_name") == 0 || strcmp(key, "subscription") == 0 || strcmp(key, "subject") == 0 || strcmp(key, "channel") == 0 || strcmp(key, "stream") == 0); } /* Target key names (endpoint URL) */ static int is_target_key(const char *key) { return (strcmp(key, "push_endpoint") == 0 || strcmp(key, "uri") == 0 || strcmp(key, "url") == 0 || strcmp(key, "endpoint") == 0 || strcmp(key, "http_target") == 0 || strcmp(key, "target_url") == 0 || strcmp(key, "webhook_url") == 0 || strcmp(key, "callback_url") == 0); } /* Infer broker type from surrounding context */ static const char *infer_broker(const char *file_path, const char *source_key) { if (strstr(file_path, "pubsub") || strstr(file_path, "pub-sub") || strstr(file_path, "pub_sub")) { return "pubsub"; } if (strstr(file_path, "scheduler") || strstr(file_path, "schedule") || strstr(file_path, "cron")) { return "cloud_scheduler"; } if (strstr(file_path, "task") || strcmp(source_key, "queue") == 0 || strcmp(source_key, "queue_name") == 0) { return "cloud_tasks"; } if (strstr(file_path, "kafka") || strcmp(source_key, "stream") == 0) { return "kafka"; } if (strstr(file_path, "sqs") || strstr(file_path, "sns")) { return "sqs"; } return "async"; } /* Scan a YAML mapping for source+target key pairs. * Collects all key-value pairs at this level and one level deep (for nested config:). */ // Strip quotes from a YAML scalar value. static char *strip_yaml_quotes(CBMArena *a, char *v) { if (!v || !v[0]) { return v; } int vlen = (int)strlen(v); if (vlen >= CBM_QUOTE_PAIR && (v[0] == '"' || v[0] == '\'')) { return cbm_arena_strndup(a, v + SKIP_ONE, (size_t)(vlen - PAIR_LEN)); } return v; } // Scan a nested YAML block_mapping for target keys (push_endpoint, uri, etc.). static void scan_nested_mapping_targets(CBMExtractCtx *ctx, TSNode val, const char **targets, int *n_targets) { uint32_t vnc = ts_node_named_child_count(val); for (uint32_t vi = 0; vi < vnc; vi++) { TSNode vc = ts_node_named_child(val, vi); if (strcmp(ts_node_type(vc), "block_mapping") != 0) { continue; } uint32_t mnc = ts_node_named_child_count(vc); for (uint32_t mi = 0; mi < mnc; mi++) { TSNode mp = ts_node_named_child(vc, mi); if (strcmp(ts_node_type(mp), "block_mapping_pair") != 0) { continue; } TSNode mk = ts_node_child_by_field_name(mp, TS_FIELD("key")); TSNode mv = ts_node_child_by_field_name(mp, TS_FIELD("value")); if (ts_node_is_null(mk) || ts_node_is_null(mv)) { continue; } char *mktext = cbm_node_text(ctx->arena, mk, ctx->source); if (mktext && is_target_key(mktext) && *n_targets < MAX_INFRA_BINDINGS) { char *mvtext = strip_yaml_quotes(ctx->arena, cbm_node_text(ctx->arena, mv, ctx->source)); if (mvtext && strstr(mvtext, "://")) { targets[(*n_targets)++] = mvtext; } } } } } // Emit infra bindings for each source × target pair combination. static void emit_infra_bindings(CBMExtractCtx *ctx, const char **sources, const char **source_keys, int n_sources, const char **targets, int n_targets) { for (int si = 0; si < n_sources; si++) { for (int ti = 0; ti < n_targets; ti++) { if (!sources[si] || !targets[ti]) { continue; } CBMInfraBinding ib = { .source_name = sources[si], .target_url = targets[ti], .broker = infer_broker(ctx->rel_path, source_keys[si]), }; cbm_infrabinding_push(&ctx->result->infra_bindings, ctx->arena, ib); } } } static void scan_mapping_for_bindings(CBMExtractCtx *ctx, TSNode mapping) { const char *sources[MAX_INFRA_BINDINGS] = {NULL}; const char *source_keys[MAX_INFRA_BINDINGS] = {NULL}; int n_sources = 0; const char *targets[MAX_INFRA_BINDINGS] = {NULL}; int n_targets = 0; uint32_t nc = ts_node_named_child_count(mapping); for (uint32_t i = 0; i < nc; i++) { TSNode pair = ts_node_named_child(mapping, i); if (strcmp(ts_node_type(pair), "block_mapping_pair") != 0) { continue; } TSNode key = ts_node_child_by_field_name(pair, TS_FIELD("key")); TSNode val = ts_node_child_by_field_name(pair, TS_FIELD("value")); if (ts_node_is_null(key) || ts_node_is_null(val)) { continue; } char *k = cbm_node_text(ctx->arena, key, ctx->source); if (!k) { continue; } const char *vtype = ts_node_type(val); if (strcmp(vtype, "block_node") != 0 && strcmp(vtype, "block_mapping") != 0) { char *v = strip_yaml_quotes(ctx->arena, cbm_node_text(ctx->arena, val, ctx->source)); if (is_source_key(k) && n_sources < MAX_INFRA_BINDINGS) { sources[n_sources] = v; source_keys[n_sources] = k; n_sources++; } if (is_target_key(k) && n_targets < MAX_INFRA_BINDINGS && v && strstr(v, "://")) { targets[n_targets++] = v; } } else { scan_nested_mapping_targets(ctx, val, targets, &n_targets); } } emit_infra_bindings(ctx, sources, source_keys, n_sources, targets, n_targets); } #define INFRA_SCAN_STACK_CAP CBM_SZ_512 static void scan_yaml_for_infra_bindings(CBMExtractCtx *ctx, TSNode root) { TSNode stack[INFRA_SCAN_STACK_CAP]; int top = 0; stack[top++] = root; while (top > 0) { TSNode node = stack[--top]; if (strcmp(ts_node_type(node), "block_mapping") == 0) { scan_mapping_for_bindings(ctx, node); } uint32_t nc = ts_node_named_child_count(node); for (int i = (int)nc - SKIP_ONE; i >= 0 && top < INFRA_SCAN_STACK_CAP; i--) { stack[top++] = ts_node_named_child(node, (uint32_t)i); } } } /* ── HCL infrastructure binding extraction ─────────────────────────── * Scan HCL block nodes (resource, dynamic) for attribute pairs * where one is a source key (topic, queue_name) and another is a * target key (uri, push_endpoint). Handles nested blocks like * push_config { push_endpoint = "..." }. */ // Extract a string value from an HCL attribute value node. The tree-sitter-hcl // grammar wraps the literal: attribute → (identifier)(expression → literal_value // → string_lit → template_literal). Descend through the expression/literal_value // wrappers to reach the actual string token before reading it. static char *extract_hcl_string_val(CBMArena *a, TSNode val_node, const char *source) { enum { HCL_MAX_UNWRAP = 5 }; for (int depth = 0; depth < HCL_MAX_UNWRAP; depth++) { const char *vk = ts_node_type(val_node); if (strcmp(vk, "expression") == 0 || strcmp(vk, "literal_value") == 0) { if (ts_node_named_child_count(val_node) == 0) { return NULL; } val_node = ts_node_named_child(val_node, 0); continue; } if (strcmp(vk, "quoted_template") == 0 || strcmp(vk, "template_literal") == 0 || strcmp(vk, "string_lit") == 0) { char *val = cbm_node_text(a, val_node, source); return strip_yaml_quotes(a, val); } return NULL; } return NULL; } // The tree-sitter-hcl grammar nests a block's attributes/sub-blocks inside a // `body` child rather than directly under the block. Return that body node so // scanners iterate the right level; fall back to the block itself if no body. static TSNode hcl_block_body(TSNode block) { TSNode body = cbm_find_child_by_kind(block, "body"); return ts_node_is_null(body) ? block : body; } // Scan a nested HCL block for target keys (push_endpoint, uri, etc.). static void scan_hcl_nested_block_targets(CBMExtractCtx *ctx, TSNode block, const char **targets, int *n_targets) { TSNode body = hcl_block_body(block); uint32_t bnc = ts_node_named_child_count(body); for (uint32_t bi = 0; bi < bnc; bi++) { TSNode bchild = ts_node_named_child(body, bi); if (strcmp(ts_node_type(bchild), "attribute") != 0) { continue; } TSNode bkey = ts_node_named_child(bchild, 0); TSNode bval = ts_node_named_child(bchild, SKIP_ONE); if (ts_node_is_null(bkey) || ts_node_is_null(bval)) { continue; } char *bk = cbm_node_text(ctx->arena, bkey, ctx->source); if (!bk || !is_target_key(bk)) { continue; } char *bv = extract_hcl_string_val(ctx->arena, bval, ctx->source); if (bv && strstr(bv, "://") && *n_targets < MAX_INFRA_BINDINGS) { targets[(*n_targets)++] = bv; } } } /* A scheduler/cron job has no topic/queue source key — its identity is the * resource itself, and the binding target is the job's invocation endpoint * (uri / http_target / pubsub_target). Detect such a block by its first label * (resource type, e.g. "google_cloud_scheduler_job") and return the job's * synthetic source name (its second label / resource name), or NULL if the * block is not a scheduler job. Sets *broker_out to the scheduler broker id. */ static const char *hcl_scheduler_source(CBMExtractCtx *ctx, TSNode block, const char **broker_out) { const char *first_label = NULL; const char *last_label = NULL; uint32_t cc = ts_node_named_child_count(block); for (uint32_t i = 0; i < cc; i++) { TSNode ch = ts_node_named_child(block, 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(ctx->arena, lit, ctx->source); if (!label || !label[0]) { continue; } if (!first_label) { first_label = label; } last_label = label; } if (!first_label) { return NULL; } /* google_cloud_scheduler_job, aws_cloudwatch_event_rule (cron), etc. */ if (strstr(first_label, "scheduler") || strstr(first_label, "schedule") || strstr(first_label, "cron")) { if (broker_out) { *broker_out = "cloud_scheduler"; } return last_label ? last_label : first_label; } return NULL; } static void scan_hcl_block_for_bindings(CBMExtractCtx *ctx, TSNode block) { const char *sources[MAX_INFRA_BINDINGS] = {NULL}; const char *source_keys[MAX_INFRA_BINDINGS] = {NULL}; int n_sources = 0; const char *targets[MAX_INFRA_BINDINGS] = {NULL}; int n_targets = 0; TSNode body = hcl_block_body(block); uint32_t nc = ts_node_named_child_count(body); for (uint32_t i = 0; i < nc; i++) { TSNode child = ts_node_named_child(body, i); const char *ck = ts_node_type(child); if (strcmp(ck, "attribute") == 0) { TSNode key_node = ts_node_named_child(child, 0); TSNode val_node = ts_node_named_child(child, SKIP_ONE); if (ts_node_is_null(key_node) || ts_node_is_null(val_node)) { continue; } char *key = cbm_node_text(ctx->arena, key_node, ctx->source); if (!key) { continue; } char *val = extract_hcl_string_val(ctx->arena, val_node, ctx->source); if (!val || !val[0]) { continue; } if (is_source_key(key) && n_sources < MAX_INFRA_BINDINGS) { sources[n_sources] = val; source_keys[n_sources] = key; n_sources++; } if (is_target_key(key) && n_targets < MAX_INFRA_BINDINGS && strstr(val, "://")) { targets[n_targets++] = val; } } else if (strcmp(ck, "block") == 0) { scan_hcl_nested_block_targets(ctx, child, targets, &n_targets); } } /* Scheduler/cron jobs carry no topic/queue source key — the resource itself * is the source. If we found an invocation target (uri/http_target) but no * explicit source key, synthesize the source from the scheduler resource so * the job→endpoint binding (INFRA_MAPS) still forms. */ if (n_sources == 0 && n_targets > 0) { const char *sched_broker = NULL; const char *sched_src = hcl_scheduler_source(ctx, block, &sched_broker); if (sched_src) { for (int ti = 0; ti < n_targets; ti++) { if (!targets[ti]) { continue; } CBMInfraBinding ib = { .source_name = sched_src, .target_url = targets[ti], .broker = sched_broker ? sched_broker : "cloud_scheduler", }; cbm_infrabinding_push(&ctx->result->infra_bindings, ctx->arena, ib); } return; } } emit_infra_bindings(ctx, sources, source_keys, n_sources, targets, n_targets); } /* Handle YAML files: walk top-level block_mapping recursively */ static void handle_yaml_nested(CBMExtractCtx *ctx, TSNode node) { if (ctx->language != CBM_LANG_YAML) { return; } const char *kind = ts_node_type(node); if (strcmp(kind, "block_mapping") != 0) { return; } /* Only process root-level block_mapping (depth 0 or 1) */ TSNode parent = ts_node_parent(node); if (ts_node_is_null(parent)) { walk_yaml_mapping(ctx, node, NULL); } else { const char *pk = ts_node_type(parent); if (strcmp(pk, "stream") == 0 || strcmp(pk, "document") == 0 || strcmp(pk, "block_node") == 0) { walk_yaml_mapping(ctx, node, NULL); } } } // --- Main unified cursor walk --- // Scan infra bindings for YAML/JSON/HCL languages. static void scan_infra_bindings(CBMExtractCtx *ctx, TSNode node) { if (ctx->language == CBM_LANG_YAML || ctx->language == CBM_LANG_JSON) { const char *nk = ts_node_type(node); if (strcmp(nk, "block_sequence") == 0 || strcmp(nk, "block_mapping") == 0 || strcmp(nk, "array") == 0 || strcmp(nk, "document") == 0) { scan_yaml_for_infra_bindings(ctx, node); } } else if (ctx->language == CBM_LANG_HCL) { if (strcmp(ts_node_type(node), "block") == 0) { scan_hcl_block_for_bindings(ctx, node); } } } // JS/TS `export_statement` appears in import_node_types so re-exports // (`export { X } from './m'`) are treated as an import boundary. But it also // wraps exported *declarations* (`export function f(cfg: Config) {}`), and // treating those as an import boundary wrongly suppresses USAGE edges for type // references inside the exported declaration's signature. Return true when the // node is an export that contains a declaration child (i.e. NOT a bare re-export), // so the caller skips the import-scope push for it. static bool is_export_of_declaration(TSNode node) { if (strcmp(ts_node_type(node), "export_statement") != 0) { return false; } uint32_t n = ts_node_child_count(node); for (uint32_t i = 0; i < n; i++) { const char *ck = ts_node_type(ts_node_child(node, i)); if (strcmp(ck, "function_declaration") == 0 || strcmp(ck, "class_declaration") == 0 || strcmp(ck, "lexical_declaration") == 0 || strcmp(ck, "abstract_class_declaration") == 0 || strcmp(ck, "interface_declaration") == 0 || strcmp(ck, "enum_declaration") == 0 || strcmp(ck, "type_alias_declaration") == 0 || strcmp(ck, "variable_declaration") == 0 || strcmp(ck, "generator_function_declaration") == 0) { return true; } } return false; } // Push scope markers for function, class, call, and import boundary nodes. static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state, uint32_t depth) { if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) { /* OCaml: a nested local `let x = e in ...` is itself a value_definition, * but the def walk does not descend into function bodies, so it emits no * node for it. Pushing a func scope here would attribute in-body calls to * that nodeless local binding — the CALLS edge then sources to neither a * Function nor the Module. Only the OUTERMOST value_definition pushes a * scope (none already on the stack), matching what the def walk extracts. */ bool skip_nested = false; if (ctx->language == CBM_LANG_OCAML) { for (int i = 0; i < state->scope_top; i++) { if (state->scopes[i].kind == SCOPE_FUNC) { skip_nested = true; break; } } } if (!skip_nested) { const char *fqn = compute_func_qn(ctx, node, spec, state); if (fqn) { push_scope(state, SCOPE_FUNC, depth, fqn); } } } else if (spec->class_node_types && cbm_kind_in_set(node, spec->class_node_types)) { const char *cqn = compute_class_qn(ctx, node, state); if (cqn) { push_scope(state, SCOPE_CLASS, depth, cqn); } } else if (ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) { TSNode type_node = ts_node_child_by_field_name(node, TS_FIELD("type")); if (!ts_node_is_null(type_node)) { char *type_name = cbm_node_text(ctx->arena, type_node, ctx->source); if (type_name && type_name[0]) { const char *tqn = cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, type_name); push_scope(state, SCOPE_CLASS, depth, tqn); } } } else if (ctx->language == CBM_LANG_DART && strcmp(ts_node_type(node), "function_body") == 0) { /* Dart models a function as `function_signature` + `function_body` SIBLINGS * (the signature node does not contain the body). A scope pushed at the * signature never covers the body, so in-body calls source to the Module. * Push the function scope at the BODY using the preceding signature * sibling's QN, so the body's children attribute to the function. */ TSNode prev = ts_node_prev_sibling(node); while (!ts_node_is_null(prev) && strcmp(ts_node_type(prev), "function_signature") != 0 && strcmp(ts_node_type(prev), "method_signature") != 0) { prev = ts_node_prev_sibling(prev); } if (!ts_node_is_null(prev)) { const char *fqn = compute_func_qn(ctx, prev, spec, state); if (fqn) { push_scope(state, SCOPE_FUNC, depth, fqn); } } } if (spec->call_node_types && cbm_kind_in_set(node, spec->call_node_types)) { push_scope(state, SCOPE_CALL, depth, NULL); } if (spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types) && !is_export_of_declaration(node)) { push_scope(state, SCOPE_IMPORT, depth, NULL); } /* Loop / branch nesting for bottleneck metrics. Loops are gated on named * nodes so anonymous `for`/`while` keyword tokens don't count. A loop is NOT * also counted as a branch (many specs list loops in branching_node_types, * but a loop is not a base-case guard for the unguarded-recursion signal). */ if (ts_node_is_named(node) && cbm_is_loop_node_type(ts_node_type(node))) { push_scope(state, SCOPE_LOOP, depth, NULL); } else if (spec->branching_node_types && cbm_kind_in_set(node, spec->branching_node_types)) { push_scope(state, SCOPE_BRANCH, depth, NULL); } } void cbm_extract_unified(CBMExtractCtx *ctx) { const CBMLangSpec *spec = cbm_lang_spec(ctx->language); if (!spec) { return; } TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); WalkState state; memset(&state, 0, sizeof(state)); uint32_t depth = 0; for (;;) { TSNode node = ts_tree_cursor_current_node(&cursor); pop_expired_scopes(&state, depth); recompute_state(&state, ctx->module_qn); handle_string_constants(ctx, node, &state); handle_calls(ctx, node, spec, &state); handle_usages(ctx, node, spec, &state); handle_throws(ctx, node, spec, &state); handle_readwrites(ctx, node, spec, &state); handle_type_refs(ctx, node, spec, &state); handle_env_accesses(ctx, node, spec, &state); handle_type_assigns(ctx, node, spec, &state); handle_string_refs(ctx, node, &state); handle_yaml_nested(ctx, node); scan_infra_bindings(ctx, node); push_boundary_scopes(ctx, node, spec, &state, depth); if (ts_tree_cursor_goto_first_child(&cursor)) { depth++; continue; } if (ts_tree_cursor_goto_next_sibling(&cursor)) { continue; } bool found = false; while (ts_tree_cursor_goto_parent(&cursor)) { depth--; if (ts_tree_cursor_goto_next_sibling(&cursor)) { found = true; break; } } if (!found) { break; } } ts_tree_cursor_delete(&cursor); }