Files
2026-11-01 00:05:24 +01:00

2936 lines
128 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "extract_unified.h"
#include "arena.h" // cbm_arena_sprintf
#include "cbm.h" // CBMExtractCtx
#include "helpers.h"
#include "extract_url_fold.h" // cbm_url_fold_record_constant (#706, #1147)
#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"
#include "foundation/compat.h" // cbm_thread_cpu_time_ns
#include <stdlib.h>
enum { MAX_INFRA_BINDINGS = 8 };
#include <stdint.h> // uint32_t, uint8_t
#include <string.h>
#include <strings.h> // strcasecmp (ObjectScript type inference)
// --- Scope stack management ---
static bool ensure_scope_capacity(WalkState *state) {
if (state->scope_top < state->scope_capacity) {
return true;
}
int new_capacity = state->scope_capacity > 0 ? state->scope_capacity * 2 : MAX_SCOPES;
CBMWalkScope *grown =
(CBMWalkScope *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memcpy(grown, state->scopes, (size_t)state->scope_top * sizeof(*grown));
state->scopes = grown;
state->scope_capacity = new_capacity;
return true;
}
static const CBMLexicalScope *lexical_scope_by_id(const WalkState *state, uint32_t id) {
return state && id > 0 && id <= (uint32_t)state->lexical_scope_count
? &state->lexical_scopes[id - 1U]
: NULL;
}
static CBMLexicalScope *mutable_lexical_scope_by_id(WalkState *state, uint32_t id) {
return state && id > 0 && id <= (uint32_t)state->lexical_scope_count
? &state->lexical_scopes[id - 1U]
: NULL;
}
static uint32_t current_lexical_scope_id(const WalkState *state) {
if (!state) {
return 0;
}
for (int i = state->scope_top - 1; i >= 0; i--) {
if (state->scopes[i].lexical_scope_id != 0) {
return state->scopes[i].lexical_scope_id;
}
}
return state->root_lexical_scope_id;
}
static bool ensure_lexical_scope_capacity(WalkState *state) {
if (state->lexical_scope_count < state->lexical_scope_capacity) {
return true;
}
if (state->lexical_scope_capacity > INT32_MAX / PAIR_LEN) {
state->lexical_binding_tracking_failed = true;
return false;
}
int new_capacity = state->lexical_scope_capacity * PAIR_LEN;
CBMLexicalScope *grown =
(CBMLexicalScope *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
state->lexical_binding_tracking_failed = true;
return false;
}
memcpy(grown, state->lexical_scopes, (size_t)state->lexical_scope_count * sizeof(*grown));
state->lexical_scopes = grown;
state->lexical_scope_capacity = new_capacity;
return true;
}
static uint32_t python_function_lookup_parent(const WalkState *state,
uint32_t structural_parent_id) {
uint32_t id = structural_parent_id;
int remaining = state ? state->lexical_scope_count : 0;
while (id != 0 && remaining-- > 0) {
const CBMLexicalScope *scope = lexical_scope_by_id(state, id);
if (!scope) {
return structural_parent_id;
}
if (scope->kind == CBM_LEXICAL_SCOPE_CLASS) {
return scope->lookup_parent_id;
}
if (scope->kind == CBM_LEXICAL_SCOPE_FUNCTION ||
scope->kind == CBM_LEXICAL_SCOPE_COMPREHENSION ||
scope->kind == CBM_LEXICAL_SCOPE_MODULE) {
return structural_parent_id;
}
id = scope->parent_id;
}
return structural_parent_id;
}
static uint32_t add_lexical_scope(WalkState *state, TSNode node, CBMLexicalScopeKind kind) {
if (!state || ts_node_is_null(node) || !ensure_lexical_scope_capacity(state)) {
return 0;
}
uint32_t parent_id = current_lexical_scope_id(state);
uint32_t lookup_parent_id = parent_id;
/* Python method/function lookup bypasses the containing class namespace;
* class attributes require qualification (`self.x`/`Cls.x`). */
if (kind == CBM_LEXICAL_SCOPE_FUNCTION && state->language == CBM_LANG_PYTHON) {
lookup_parent_id = python_function_lookup_parent(state, parent_id);
}
CBMLexicalScope *scope = &state->lexical_scopes[state->lexical_scope_count];
scope->id = (uint32_t)state->lexical_scope_count + 1U;
scope->parent_id = parent_id;
scope->lookup_parent_id = lookup_parent_id;
scope->start_byte = ts_node_start_byte(node);
scope->end_byte = ts_node_end_byte(node);
scope->kind = (uint8_t)kind;
state->lexical_scope_count++;
return scope->id;
}
/* ── #1912: Python parameter bindings held live by the walk ───────────────────
*
* A bare Python call `run()` cannot honestly resolve to a project function by
* short name when `run` is bound as a parameter of an enclosing def or lambda:
* the parameter shadows any module-level `run` for the whole body, so the match
* is fabricated by construction.
*
* Deciding that per call by ASCENDING the tree is the trap this replaces. Both
* ts_node_parent() (O(depth) per hop) and a walk-cursor ascent (O(1) per hop,
* O(depth) per call) are per-call costs, and every level of f(f(f(...))) is
* itself a bare call, so the file-wide cost is quadratic or worse -- exactly
* what CBMWalkScope's comment records for the state it replaced. Scanning the
* frame stack has the same shape.
*
* So the walk carries the answer instead: a name -> active-count map, pushed
* when a Python function or lambda scope opens and unwound when it closes.
* Lookup is O(1), which is what removes the need for a hop cap.
*
* A COUNT, not a flag: `def outer(run): def inner(run):` binds one name twice,
* and leaving the inner scope must not unbind the outer one.
*
* Every failure path answers "not bound", which can only ever cost a
* suppression -- never a true edge. */
static uint32_t py_param_hash(const char *s) {
uint32_t h = 2166136261u; /* FNV-1a */
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
h = (h ^ *p) * 16777619u;
}
return h ? h : 1u; /* 0 marks an empty slot */
}
static CBMParamSlot *py_param_slot(WalkState *state, const char *name, uint32_t hash) {
int mask = state->py_param_slot_capacity - 1;
int i = (int)(hash & (uint32_t)mask);
for (;;) {
CBMParamSlot *slot = &state->py_param_slots[i];
if (slot->hash == 0) {
return slot; /* free slot -- caller decides whether to claim it */
}
if (slot->hash == hash && strcmp(slot->name, name) == 0) {
return slot;
}
i = (i + 1) & mask;
}
}
static bool py_param_grow(WalkState *state) {
int new_capacity = state->py_param_slot_capacity * PAIR_LEN;
if (new_capacity <= state->py_param_slot_capacity) {
return false;
}
CBMParamSlot *grown =
(CBMParamSlot *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memset(grown, 0, (size_t)new_capacity * sizeof(*grown));
CBMParamSlot *old = state->py_param_slots;
int old_capacity = state->py_param_slot_capacity;
state->py_param_slots = grown;
state->py_param_slot_capacity = new_capacity;
for (int i = 0; i < old_capacity; i++) {
if (old[i].hash != 0) {
*py_param_slot(state, old[i].name, old[i].hash) = old[i];
}
}
return true;
}
static bool py_param_stack_reserve(WalkState *state) {
if (state->py_param_stack_count < state->py_param_stack_capacity) {
return true;
}
int new_capacity = state->py_param_stack_capacity * PAIR_LEN;
if (new_capacity <= state->py_param_stack_capacity) {
return false;
}
const char **grown =
(const char **)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memcpy(grown, state->py_param_stack, (size_t)state->py_param_stack_count * sizeof(*grown));
state->py_param_stack = grown;
state->py_param_stack_capacity = new_capacity;
return true;
}
/* Bind one parameter name for the lifetime of the frame currently on top. */
static void py_param_bind(WalkState *state, const char *name) {
if (state->py_param_tracking_failed || !name || !name[0]) {
return;
}
/* Grow before inserting: the probe below must always find a free slot, and
* a table above ~70% load degrades toward linear probing. */
if ((state->py_param_slot_used + 1) * 10 >= state->py_param_slot_capacity * 7) {
if (!py_param_grow(state)) {
state->py_param_tracking_failed = true;
return;
}
}
if (!py_param_stack_reserve(state)) {
state->py_param_tracking_failed = true;
return;
}
uint32_t hash = py_param_hash(name);
CBMParamSlot *slot = py_param_slot(state, name, hash);
if (slot->hash == 0) {
slot->hash = hash;
slot->name = name;
slot->count = 0;
state->py_param_slot_used++;
}
slot->count++;
state->py_param_stack[state->py_param_stack_count++] = name;
}
/* Unwind to a frame's entry height, undoing exactly what that frame bound. */
static void py_param_unwind_to(WalkState *state, int base) {
if (state->py_param_tracking_failed) {
return;
}
while (state->py_param_stack_count > base) {
const char *name = state->py_param_stack[--state->py_param_stack_count];
CBMParamSlot *slot = py_param_slot(state, name, py_param_hash(name));
if (slot->hash != 0 && slot->count > 0) {
slot->count--;
}
}
}
bool cbm_walk_python_param_is_bound(const WalkState *state, const char *name) {
if (!state || !name || !name[0] || state->py_param_tracking_failed ||
state->py_param_slot_used == 0) {
return false;
}
const CBMParamSlot *slot = py_param_slot((WalkState *)state, name, py_param_hash(name));
return slot->hash != 0 && slot->count > 0;
}
/* The identifier a Python parameter node binds. Handles the bare, typed,
* defaulted, keyword-only, *args and **kwargs shapes. */
static const char *py_parameter_name(CBMExtractCtx *ctx, TSNode param) {
if (ts_node_is_null(param)) {
return NULL;
}
if (strcmp(ts_node_type(param), "identifier") == 0) {
return cbm_node_text(ctx->arena, param, ctx->source);
}
TSNode name = ts_node_child_by_field_name(param, TS_FIELD("name"));
if (!ts_node_is_null(name) && strcmp(ts_node_type(name), "identifier") == 0) {
return cbm_node_text(ctx->arena, name, ctx->source);
}
/* `*args` / `**kwargs`, and any typed shape without a `name` field: the
* bound identifier is the first named child. */
TSNode first = ts_node_named_child(param, 0);
if (!ts_node_is_null(first) && strcmp(ts_node_type(first), "identifier") == 0) {
return cbm_node_text(ctx->arena, first, ctx->source);
}
return NULL;
}
/* Bind this node's parameters into the frame just pushed for it. Called after
* every push in push_boundary_scopes, so the top frame is this node's own and
* pop_expired_scopes unwinds them at exactly the right depth. */
static void py_bind_scope_parameters(CBMExtractCtx *ctx, TSNode node, WalkState *state) {
if (ctx->language != CBM_LANG_PYTHON || state->scope_top == 0) {
return;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "function_definition") != 0 && strcmp(kind, "lambda") != 0) {
return;
}
TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return;
}
uint32_t count = ts_node_named_child_count(params);
for (uint32_t i = 0; i < count; i++) {
py_param_bind(state, py_parameter_name(ctx, ts_node_named_child(params, i)));
}
}
static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) {
if (!ensure_scope_capacity(state)) {
return false;
}
CBMWalkScope *f = &state->scopes[state->scope_top];
f->prev_py_param_stack_count = state->py_param_stack_count;
f->kind = kind;
f->depth = depth;
f->qn = qn;
f->lexical_scope_id = 0;
f->invocation_kind = CBM_INVOCATION_NONE;
f->callee_expr = (TSNode){0};
f->callee_leaf = (TSNode){0};
/* Save the complete displaced tuple, then apply this frame's effect --
* pop_expired_scopes restores verbatim. O(1) either way; see the
* CBMWalkScope comment for the quadratic recompute this replaces. */
f->prev_enclosing_func_qn = state->enclosing_func_qn;
f->prev_enclosing_class_qn = state->enclosing_class_qn;
f->prev_enclosing_class_node = state->enclosing_class_node;
f->prev_invocation_kind = state->invocation_kind;
f->prev_callee_expr = state->callee_expr;
f->prev_callee_leaf = state->callee_leaf;
f->prev_inside_import = state->inside_import;
f->prev_loop_depth = state->loop_depth;
f->prev_branch_depth = state->branch_depth;
switch (kind) {
case SCOPE_FUNC:
state->enclosing_func_qn = qn;
break;
case SCOPE_CLASS:
case SCOPE_NAMESPACE:
state->enclosing_class_qn = qn;
/* push_lexical_scope records the node; a node-less push has none. */
state->enclosing_class_node = (TSNode){0};
break;
case SCOPE_CALL:
/* The invocation triple is applied by push_call_scope once the caller
* has filled the frame fields. */
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;
}
state->scope_top++;
return true;
}
static bool push_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t depth, const char *qn,
TSNode node, CBMLexicalScopeKind lexical_kind) {
if (!push_scope(state, walk_kind, depth, qn)) {
state->lexical_binding_tracking_failed = true;
return false;
}
if (walk_kind == SCOPE_CLASS || walk_kind == SCOPE_NAMESPACE) {
state->enclosing_class_node = node;
}
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id =
add_lexical_scope(state, node, lexical_kind);
return true;
}
static bool push_existing_lexical_scope(WalkState *state, uint8_t walk_kind, uint32_t depth,
const char *qn, uint32_t lexical_scope_id, TSNode node) {
CBMLexicalScope *scope = mutable_lexical_scope_by_id(state, lexical_scope_id);
if (!scope || !push_scope(state, walk_kind, depth, qn)) {
state->lexical_binding_tracking_failed = true;
return false;
}
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id = lexical_scope_id;
uint32_t end_byte = ts_node_end_byte(node);
if (end_byte > scope->end_byte) {
scope->end_byte = end_byte;
}
return true;
}
static uint32_t active_same_function_scope_id(const WalkState *state, const char *function_qn,
TSNode node) {
if (!state || !function_qn) {
return 0;
}
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
for (int i = state->scope_top - 1; i >= 0; i--) {
if (state->scopes[i].kind != SCOPE_FUNC || !state->scopes[i].qn ||
strcmp(state->scopes[i].qn, function_qn) != 0) {
continue;
}
const CBMLexicalScope *scope =
lexical_scope_by_id(state, state->scopes[i].lexical_scope_id);
if (scope && scope->start_byte <= start && end <= scope->end_byte) {
return scope->id;
}
}
return 0;
}
static bool push_function_scope(WalkState *state, uint32_t depth, const char *function_qn,
TSNode node) {
uint32_t existing_id = active_same_function_scope_id(state, function_qn, node);
return existing_id ? push_existing_lexical_scope(state, SCOPE_FUNC, depth, function_qn,
existing_id, node)
: push_lexical_scope(state, SCOPE_FUNC, depth, function_qn, node,
CBM_LEXICAL_SCOPE_FUNCTION);
}
static void push_call_scope(WalkState *state, uint32_t depth,
const CBMInvocationDescriptor *invocation) {
if (!invocation ||
(invocation->kind != CBM_INVOCATION_CALLABLE_REFERENCE && !invocation->raw_call_emitted) ||
(ts_node_is_null(invocation->callee_expr) && ts_node_is_null(invocation->callee_leaf))) {
return;
}
if (!push_scope(state, SCOPE_CALL, depth, NULL)) {
return;
}
state->scopes[state->scope_top - SKIP_ONE].invocation_kind = invocation->kind;
state->scopes[state->scope_top - SKIP_ONE].callee_expr = invocation->callee_expr;
state->scopes[state->scope_top - SKIP_ONE].callee_leaf = invocation->callee_leaf;
/* Apply the CALL effect (push_scope saved the displaced tuple already;
* the frame fields had to be filled first). */
state->invocation_kind = invocation->kind;
state->callee_expr = invocation->callee_expr;
state->callee_leaf = invocation->callee_leaf;
}
// Pop scopes that we've ascended out of (depth >= current cursor depth),
// restoring the walk-state tuple each frame displaced. LIFO order keeps the
// restores exact; O(1) per frame.
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) {
const CBMWalkScope *f = &state->scopes[--state->scope_top];
state->enclosing_func_qn = f->prev_enclosing_func_qn;
state->enclosing_class_qn = f->prev_enclosing_class_qn;
state->enclosing_class_node = f->prev_enclosing_class_node;
state->invocation_kind = f->prev_invocation_kind;
state->callee_expr = f->prev_callee_expr;
state->callee_leaf = f->prev_callee_leaf;
state->inside_import = f->prev_inside_import;
state->loop_depth = f->prev_loop_depth;
state->branch_depth = f->prev_branch_depth;
py_param_unwind_to(state, f->prev_py_param_stack_count);
}
}
// 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) {
bool chialisp = (ctx->language == CBM_LANG_CHIALISP);
if (ts_node_named_child_count(node) < 2) {
return NULL;
}
/* Chialisp reads its head and name through the comment-skipping accessor —
* the SAME one extract_lisp_def() uses. When these two disagree (one
* skipping comments, the other not), a doc comment between a def head and
* its name shifts the named-child indices for only one of them, and the
* call scope silently stops matching the def it belongs to. */
TSNode head_node =
chialisp ? cbm_lisp_named_child_skip_comments(node, 0) : ts_node_named_child(node, 0);
if (ts_node_is_null(head_node)) {
return NULL;
}
char *head = cbm_node_text(ctx->arena, head_node, ctx->source);
if (!(chialisp ? cbm_chialisp_is_def_head(head) : lisp_head_is_def(head))) {
return NULL;
}
if (chialisp && cbm_lisp_node_in_quote(ctx->arena, node, ctx->source)) {
return NULL;
}
if (chialisp && head && strcmp(head, "mod") == 0) {
/* `mod`'s second form is the curried-arg list, not a name; the puzzle
* entry point is named by its file, so in-body top-level calls
* attribute to the entry rather than to a curried argument. Mirrors the
* lisp_path_stem() naming in extract_defs.c. */
const char *path = ctx->rel_path;
const char *slash = path ? strrchr(path, '/') : NULL;
const char *base = slash ? slash + 1 : path;
if (!base || !base[0]) {
return NULL;
}
const char *dot = strrchr(base, '.');
size_t len = (dot && dot != base) ? (size_t)(dot - base) : strlen(base);
char *stem = cbm_arena_strndup(ctx->arena, base, len);
return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, stem);
}
TSNode target =
chialisp ? cbm_lisp_named_child_skip_comments(node, 1) : ts_node_named_child(node, 1);
if (ts_node_is_null(target)) {
return NULL;
}
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;
}
/* A guard wraps the whole head in a `when` operator, so the head naming the
* function is its left operand. Left wrapped, this returns NULL and the def
* opens NO function scope: every call in a guarded body then sources to the
* FILE node and the function itself reports no outgoing edges. */
first_arg = cbm_elixir_def_head_unwrap_guard(first_arg);
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 <cffunction name="foo">
* 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);
}
// --- ObjectScript variable type inference (instance_method_call resolution) ---
// Insert or update var_name -> class_name. Silent on overflow.
static void os_type_map_add(os_type_map_t *map, const char *var_name, const char *class_name) {
if (map->count >= OS_TYPE_MAP_CAP || !var_name || !class_name) {
return;
}
for (int i = 0; i < map->count; i++) {
if (strcmp(map->entries[i].var_name, var_name) == 0) {
map->entries[i].class_name = class_name;
return;
}
}
map->entries[map->count].var_name = var_name;
map->entries[map->count].class_name = class_name;
map->count++;
}
// Locate the class_method_call inside an RHS expression (peeking through a
// couple of common ObjectScript expression container node types).
static TSNode find_class_method_call(TSNode root, const char *end) {
(void)end;
if (strcmp(ts_node_type(root), "class_method_call") == 0) {
return root;
}
static const char *containers[] = {"expression", "expr_atom", NULL};
for (const char **c = containers; *c; c++) {
TSNode inner = cbm_find_child_by_kind(root, *c);
if (!ts_node_is_null(inner)) {
TSNode hit = cbm_find_child_by_kind(inner, "class_method_call");
if (!ts_node_is_null(hit)) {
return hit;
}
TSNode inner2 = cbm_find_child_by_kind(inner, "expr_atom");
if (!ts_node_is_null(inner2)) {
hit = cbm_find_child_by_kind(inner2, "class_method_call");
if (!ts_node_is_null(hit)) {
return hit;
}
}
}
}
return cbm_find_child_by_kind(root, "class_method_call");
}
// On a `Set var = ##class(X).%New()` (or %OpenId/%Open, or a method whose
// return type is known) map var -> X. On a class `Property`/`Relationship`,
// map `..PropName -> typename` (surviving method-scope resets).
static void handle_objectscript_type_map(CBMExtractCtx *ctx, TSNode node, WalkState *state) {
if (ctx->language != CBM_LANG_OBJECTSCRIPT_UDL &&
ctx->language != CBM_LANG_OBJECTSCRIPT_ROUTINE) {
return;
}
const char *nk = ts_node_type(node);
if (strcmp(nk, "command_set") == 0) {
for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) {
TSNode set_arg = ts_node_named_child(node, i);
const char *sak = ts_node_type(set_arg);
if (strcmp(sak, "set_argument") != 0 && strcmp(sak, "assignment") != 0) {
continue;
}
TSNode lhs = {0};
TSNode rhs = {0};
for (uint32_t j = 0; j < ts_node_named_child_count(set_arg); j++) {
TSNode achild = ts_node_named_child(set_arg, j);
const char *ak = ts_node_type(achild);
if (strcmp(ak, "set_target") == 0 || strcmp(ak, "lvn") == 0 ||
strcmp(ak, "variable") == 0 || strcmp(ak, "glvn") == 0) {
lhs = achild;
} else if (strcmp(ak, "expression") == 0 || strcmp(ak, "expr_atom") == 0 ||
strcmp(ak, "class_method_call") == 0) {
rhs = achild;
}
}
if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) {
continue;
}
TSNode cm_call = find_class_method_call(rhs, NULL);
if (ts_node_is_null(cm_call)) {
continue;
}
TSNode method_name_node = cbm_find_child_by_kind(cm_call, "method_name");
if (ts_node_is_null(method_name_node)) {
continue;
}
TSNode mn_ident = ts_node_named_child_count(method_name_node) > 0
? ts_node_named_child(method_name_node, 0)
: (TSNode){0};
if (ts_node_is_null(mn_ident)) {
continue;
}
char *method_text = cbm_node_text(ctx->arena, mn_ident, ctx->source);
if (!method_text) {
continue;
}
TSNode class_ref = cbm_find_child_by_kind(cm_call, "class_ref");
if (ts_node_is_null(class_ref)) {
continue;
}
TSNode cname = cbm_find_child_by_kind(class_ref, "class_name");
if (ts_node_is_null(cname)) {
continue;
}
char *cls = cbm_node_text(ctx->arena, cname, ctx->source);
if (!cls || !cls[0]) {
continue;
}
bool is_constructor =
(strcasecmp(method_text, "%New") == 0 || strcasecmp(method_text, "%OpenId") == 0 ||
strcasecmp(method_text, "%Open") == 0);
if (!is_constructor) {
if (!ctx->return_type_table) {
continue;
}
char *method_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", cls, method_text);
for (int rti = 0; rti < ctx->return_type_table->count; rti++) {
if (strcasecmp(ctx->return_type_table->entries[rti].method_qn, method_qn) ==
0) {
cls = cbm_arena_strdup(ctx->arena,
ctx->return_type_table->entries[rti].return_type);
is_constructor = true;
break;
}
}
if (!is_constructor) {
continue;
}
}
TSNode var_node = lhs;
TSNode inner = cbm_find_child_by_kind(lhs, "glvn");
if (!ts_node_is_null(inner)) {
var_node = inner;
}
inner = cbm_find_child_by_kind(var_node, "lvn");
if (!ts_node_is_null(inner)) {
var_node = inner;
}
char *var = cbm_node_text(ctx->arena, var_node, ctx->source);
if (!var || !var[0]) {
continue;
}
os_type_map_add(&state->os_type_map, var, cls);
}
}
if (strcmp(nk, "property") == 0 || strcmp(nk, "relationship") == 0) {
TSNode prop_name_node = cbm_find_child_by_kind(node, "property_name");
if (ts_node_is_null(prop_name_node)) {
prop_name_node = cbm_find_child_by_kind(node, "relationship_name");
}
TSNode ret_type = cbm_find_child_by_kind(node, "return_type");
if (!ts_node_is_null(prop_name_node) && !ts_node_is_null(ret_type)) {
TSNode tname = cbm_find_child_by_kind(ret_type, "typename");
if (!ts_node_is_null(tname)) {
char *pname = cbm_node_text(ctx->arena, prop_name_node, ctx->source);
char *ptype = cbm_node_text(ctx->arena, tname, ctx->source);
if (pname && pname[0] && ptype && ptype[0]) {
char *dot_name = cbm_arena_sprintf(ctx->arena, "..%s", pname);
os_type_map_add(&state->os_type_map, dot_name, ptype);
state->os_type_map.class_base_count = state->os_type_map.count;
}
}
}
}
}
// Resolve the FQN of an ObjectScript class_definition node (via its class_name).
static const char *objectscript_get_class_name(CBMExtractCtx *ctx, TSNode node) {
for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) {
TSNode child = ts_node_named_child(node, i);
if (strcmp(ts_node_type(child), "class_name") == 0) {
char *name = cbm_node_text(ctx->arena, child, ctx->source);
if (name && name[0]) {
return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
}
}
}
return NULL;
}
// Resolve the QN of an ObjectScript method/classmethod/query node for scope tracking.
static const char *objectscript_get_method_qn(CBMExtractCtx *ctx, TSNode node,
const char *enclosing_class_qn) {
const char *nk = ts_node_type(node);
if (strcmp(nk, "query") == 0) {
TSNode query_name = cbm_find_child_by_kind(node, "query_name");
if (ts_node_is_null(query_name)) {
return NULL;
}
char *name = cbm_node_text(ctx->arena, query_name, ctx->source);
if (!name || !name[0]) {
return NULL;
}
return enclosing_class_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", enclosing_class_qn, name)
: cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
}
if (strcmp(nk, "method") != 0 && strcmp(nk, "classmethod") != 0) {
return NULL;
}
for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) {
TSNode child = ts_node_named_child(node, i);
if (strcmp(ts_node_type(child), "method_definition") == 0) {
for (uint32_t j = 0; j < ts_node_named_child_count(child); j++) {
TSNode mchild = ts_node_named_child(child, j);
if (strcmp(ts_node_type(mchild), "method_name") == 0) {
if (ts_node_named_child_count(mchild) > 0) {
TSNode ident = ts_node_named_child(mchild, 0);
char *name = cbm_node_text(ctx->arena, ident, ctx->source);
if (name && name[0]) {
if (enclosing_class_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", enclosing_class_qn,
name);
}
return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
}
}
}
}
}
}
return NULL;
}
/* Python: the nearest named scope around the node -- a def or a class. A def
* or class nested in a def is qualified by that def (proj.mod.create_app.index),
* exactly as extract_defs.c mints it; enclosing_class_qn alone would skip the
* def and hand a def nested in a method its method's QN. Lambdas push no named
* frame, so they stay transparent. */
static const char *py_nearest_scope_qn(const WalkState *state) {
for (int i = state->scope_top - SKIP_ONE; i >= 0; i--) {
const CBMWalkScope *f = &state->scopes[i];
if ((f->kind == SCOPE_FUNC || f->kind == SCOPE_CLASS) && f->qn) {
return f->qn;
}
}
return NULL;
}
// 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) {
/* Raw configured definitions already own the canonical QN, including
* namespace scope. Never rederive it from the bare macro identifier. */
const char *configured_qn = cbm_test_definition_qn(ctx, node);
if (configured_qn) {
return configured_qn;
}
(void)spec;
if (ctx->language == CBM_LANG_WOLFRAM) {
return compute_wolfram_func_qn(ctx, node);
}
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) {
return objectscript_get_method_qn(ctx, node, state->enclosing_class_qn);
}
if (ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
const char *kind = ts_node_type(node);
TSNode tag = {0};
if (strcmp(kind, "procedure") == 0) {
tag = cbm_find_child_by_kind(node, "tag");
} else if (strcmp(kind, "tag") == 0) {
/* The enclosing procedure owns the full callable range and already
* pushed this QN. Do not create a nested label-only function scope. */
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "procedure") != 0) {
tag = node;
}
}
if (!ts_node_is_null(tag)) {
char *name = cbm_node_text(ctx->arena, tag, ctx->source);
if (name && name[0]) {
return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
}
}
return NULL;
}
/* CFML tag dialect: <cffunction name="foo"> 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 || ctx->language == CBM_LANG_CHIALISP) {
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, ctx->language);
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);
}
}
/* Go method `func (s *Storage) Close() {...}`: the def extractor records
* this as Method "proj.pkg.Storage.Close" (receiver-qualified, mirroring
* the C++ out-of-line rule above). The call-scope QN must match — a bare
* "proj.pkg.Close" names a node that no longer exists, so every call
* inside the method body would fall back to File-node attribution
* (calls_find_source). Same ONE-formula contract as the def side:
* cbm_go_receiver_type_name + cbm_fqn_compute_source_lang. */
if (ctx->language == CBM_LANG_GO && strcmp(ts_node_type(node), "method_declaration") == 0) {
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
if (!ts_node_is_null(recv)) {
char *recv_type = cbm_go_receiver_type_name(ctx->arena, recv, ctx->source);
if (recv_type && recv_type[0]) {
const char *type_qn = cbm_fqn_compute_source_lang(
ctx->arena, ctx->project, ctx->rel_path, recv_type, ctx->language);
/* NULL only on allocation failure: fall through to the flat
* QN, as the def side does (extract_defs.c). */
if (type_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, name);
}
}
}
}
/* Go init(): the def extractor folds the file basename and the init's
* per-file ordinal into the QN so every init in a package survives the
* upsert (#1910, the #495 cfg-twin pattern; ordinal, not line, so the QN
* is stable under edits above the function). Name it through the same
* cbm_go_init_qn, or init-body calls fall back to File-node attribution
* (calls_find_source). */
if (ctx->language == CBM_LANG_GO && strcmp(name, "init") == 0 &&
strcmp(ts_node_type(node), "function_declaration") == 0) {
const char *base_qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path,
name, ctx->language);
/* NULL only on allocation failure: fall through to the flat QN, as
* the receiver branch above does. */
if (base_qn) {
return cbm_go_init_qn(ctx->arena, base_qn, ctx->rel_path, node, ctx->source);
}
}
/* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def
* extractor bakes it into the def QN. Compose it identically here — otherwise
* an in-body call sources to a QN one or more segments short of the def, and
* the edge is dropped at write. */
const char *qn_name = name;
if (ctx->language == CBM_LANG_NIX) {
qn_name = cbm_nix_qn_name(ctx->arena, node, ctx->source, name);
if (!qn_name || !qn_name[0]) {
return NULL;
}
}
const char *outer_qn =
ctx->language == CBM_LANG_PYTHON ? py_nearest_scope_qn(state) : state->enclosing_class_qn;
if (outer_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", outer_qn, 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, qn_name,
ctx->language);
}
// Compute class QN for scope tracking.
static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkState *state) {
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) {
return objectscript_get_class_name(ctx, node);
}
/* Nix: an attrset-valued `binding` is a named scope. Same shared helper the def
* extractor's compute_class_qn uses — these are two separate functions, and a
* one-segment disagreement between them drops every CALLS edge sourced from a
* nested binding. */
if (ctx->language == CBM_LANG_NIX) {
return cbm_nix_binding_scope_qn(ctx, node, state ? state->enclosing_class_qn : NULL);
}
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;
}
/* Rust: the impl's type text carries its generic arguments (`impl<T> Gen<T>`,
* `impl<'a> Parser<'a>`), which the def side strips before naming the type
* (extract_defs.c extract_rust_impl: `Buffer<T>` -> `Buffer`). Strip them the
* same way, or every method of a generic impl gets a scope QN no node carries
* and all its calls fall back to the File node. */
if (ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) {
char *lt = strchr(name, '<');
if (lt == name) {
return NULL; /* `<T as Tr>`-style self type: no name to scope by */
}
if (lt) {
*lt = '\0';
}
}
/* 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.
* Python: a class inside a def is qualified by the def (py_nearest_scope_qn). */
const char *outer_qn = NULL;
if (state) {
outer_qn = ctx->language == CBM_LANG_PYTHON ? py_nearest_scope_qn(state)
: state->enclosing_class_qn;
}
if (outer_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", outer_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;
}
/* Python and JS/TS fold the value: `BASE + "/path"`, `${BASE}/path`,
* f"{BASE}/path" and object-literal endpoint maps (#706, #1147). */
if (cbm_url_fold_lang(ctx->language)) {
cbm_url_fold_record_constant(ctx, cbm_node_text(ctx->arena, name_node, ctx->source),
value_node);
return;
}
/* Value must be a string literal (template literals flatten to "{}" form) */
const char *value_kind = ts_node_type(value_node);
const char *flat_value = NULL;
if (strcmp(value_kind, "template_string") == 0) {
flat_value = cbm_template_string_text(ctx->arena, value_node, ctx->source);
if (!flat_value) {
return;
}
} else if (!is_string_node(value_kind)) {
return;
}
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
char *value =
flat_value ? (char *)flat_value : cbm_node_text(ctx->arena, value_node, ctx->source);
if (!name || !name[0] || !value || !value[0]) {
return;
}
/* Strip quotes from value (template values are already unquoted) */
int vlen = (int)strlen(value);
if (!flat_value && 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);
/* JS/TS template literals: flatten ${...} substitutions to "{}" so URL-ish
* template strings become string_refs with the canonical placeholder shape
* shared with server route paths (issue #1006). */
if (strcmp(kind, "template_string") == 0) {
const char *flat = cbm_template_string_text(ctx->arena, node, ctx->source);
if (!flat) {
return;
}
int kind_val = cbm_classify_string(flat, (int)strlen(flat));
if (kind_val < 0) {
return;
}
CBMStringRef ref = {
.value = flat,
.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);
return;
}
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);
}
// --- URL-builder helpers (issue #1009) ---
/* Map-aware template flatten for builder bodies: a ${...} substitution that is
* a bare identifier or a call to an already-recorded name (const or an earlier
* builder in the same file) inlines that value; anything else becomes "{}".
* The query string is not part of a route's identity, so the result is
* truncated at the first '?'. Handles the composed-builder shape
* `return \`${basePath(id)}?${params}\``. */
static const char *builder_template_text(CBMExtractCtx *ctx, TSNode node) {
enum { BLD_BUF = 512 };
char buf[BLD_BUF];
size_t pos = 0;
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(node, i);
const char *k = ts_node_type(c);
const char *piece = NULL;
if (strcmp(k, "string_fragment") == 0) {
piece = cbm_node_text(ctx->arena, c, ctx->source);
} else if (strcmp(k, "template_substitution") == 0) {
piece = "{}";
if (ts_node_named_child_count(c) > 0) {
TSNode expr = ts_node_named_child(c, 0);
/* `${basePath(id)}` inlines a builder, `${BASE}` a const. A bare
* name never resolves to a builder: that reads the function. */
bool want_builder = strcmp(ts_node_type(expr), "call_expression") == 0;
TSNode name_node =
want_builder ? ts_node_child_by_field_name(expr, TS_FIELD("function")) : expr;
if (!ts_node_is_null(name_node) &&
strcmp(ts_node_type(name_node), "identifier") == 0) {
char *nm = cbm_node_text(ctx->arena, name_node, ctx->source);
if (nm) {
const CBMStringConstantMap *map = &ctx->string_constants;
for (int mi = 0; mi < map->count; mi++) {
if (map->values[mi] && map->is_url_builder[mi] == want_builder &&
strcmp(map->names[mi], nm) == 0) {
piece = map->values[mi];
break;
}
}
}
}
}
} else {
continue;
}
if (!piece) {
continue;
}
size_t pl = strlen(piece);
if (pos + pl >= BLD_BUF) {
return NULL;
}
memcpy(buf + pos, piece, pl);
pos += pl;
}
/* Route identity excludes the query string. */
for (size_t qi = 0; qi < pos; qi++) {
if (buf[qi] == '?') {
pos = qi;
break;
}
}
if (pos == 0) {
return NULL;
}
return cbm_arena_strndup(ctx->arena, buf, pos);
}
/* Flatten a string-ish node (plain string or template literal) to text. */
static const char *url_builder_literal_text(CBMExtractCtx *ctx, TSNode value_node) {
const char *kind = ts_node_type(value_node);
if (strcmp(kind, "template_string") == 0) {
return builder_template_text(ctx, value_node);
}
if (!is_string_node(kind)) {
return NULL;
}
char *text = cbm_node_text(ctx->arena, value_node, ctx->source);
if (!text || !text[0]) {
return NULL;
}
int len = (int)strlen(text);
if (len >= CBM_QUOTE_PAIR && (text[0] == '"' || text[0] == '\'')) {
text = cbm_arena_strndup(ctx->arena, text + SKIP_ONE, (size_t)(len - PAIR_LEN));
}
return text;
}
/* A route-shaped URL literal: an absolute path that classifies as a URL. */
static const char *builder_route_url(CBMExtractCtx *ctx, TSNode value_node) {
const char *url = url_builder_literal_text(ctx, value_node);
if (!url || url[0] != '/' || cbm_classify_string(url, (int)strlen(url)) != CBM_STRREF_URL) {
return NULL;
}
return url;
}
static void record_url_builder(CBMExtractCtx *ctx, const char *name, const char *url) {
if (!name || !name[0] || !url) {
return;
}
CBMStringConstantMap *map = &ctx->string_constants;
for (int i = 0; i < map->count; i++) {
if (strcmp(map->names[i], name) == 0) {
if (map->is_url_builder[i] && map->values[i] && strcmp(map->values[i], url) != 0) {
map->values[i] = NULL; /* ambiguous builder */
}
return;
}
}
if (map->count < CBM_MAX_STRING_CONSTANTS) {
map->names[map->count] = (char *)name;
map->values[map->count] = (char *)url;
map->is_url_builder[map->count] = true;
map->count++;
}
}
/* Every `return` this function owns yields a route-shaped URL literal. A body
* that also returns a computed path (`return computePath(kind)`) would attribute
* its one literal to call sites that never produce it, so a mixed builder is
* declined rather than guessed. Returns inside a nested function belong to that
* function, not to this one. */
static bool builder_returns_only_urls(CBMExtractCtx *ctx, TSNode func) {
TSNode body = ts_node_child_by_field_name(func, TS_FIELD("body"));
if (ts_node_is_null(body)) {
return false;
}
bool only_urls = true;
TSTreeCursor cursor = ts_tree_cursor_new(body);
for (;;) {
TSNode node = ts_tree_cursor_current_node(&cursor);
if (strcmp(ts_node_type(node), "return_statement") == 0 &&
ts_node_eq(cbm_find_enclosing_func(node, ctx->language), func)) {
if (ts_node_named_child_count(node) == 0 ||
!builder_route_url(ctx, ts_node_named_child(node, 0))) {
only_urls = false;
break;
}
}
if (ts_tree_cursor_goto_first_child(&cursor)) {
continue;
}
if (ts_tree_cursor_goto_next_sibling(&cursor)) {
continue;
}
bool found = false;
while (ts_tree_cursor_goto_parent(&cursor)) {
if (ts_tree_cursor_goto_next_sibling(&cursor)) {
found = true;
break;
}
}
if (!found) {
break;
}
}
ts_tree_cursor_delete(&cursor);
return only_urls;
}
/* URL-builder helper pattern (issue #1009): a small function whose return value
* is a URL-shaped literal, consumed as `client(buildPath(id))`. The literal
* never appears as a call argument, so first_string_arg resolution cannot see
* it; record `functionName -> url` in the per-file constant map and let the
* call-site call_expression branch resolve it. Covers `return`-statement bodies
* and arrow-function expression bodies.
*
* JS/TS only. The predicate accepts any absolute pathname, and `return` plus a
* string literal is also the shape of every C or Go helper handing back a
* filesystem path (`/etc/...`, `/proc/self/...`); recording those would mint a
* Route node and an HTTP_CALLS edge in a language that speaks no HTTP. */
static void handle_url_builders(CBMExtractCtx *ctx, TSNode node, const WalkState *state) {
if (ctx->language != CBM_LANG_JAVASCRIPT && ctx->language != CBM_LANG_TYPESCRIPT &&
ctx->language != CBM_LANG_TSX) {
return;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "arrow_function") == 0) {
TSNode body = ts_node_child_by_field_name(node, TS_FIELD("body"));
if (ts_node_is_null(body) || strcmp(ts_node_type(body), "statement_block") == 0) {
return;
}
const char *url = builder_route_url(ctx, body);
if (!url) {
return;
}
TSNode parent = ts_node_parent(node);
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) {
TSNode name_node = ts_node_child_by_field_name(parent, TS_FIELD("name"));
if (!ts_node_is_null(name_node)) {
record_url_builder(ctx, cbm_node_text(ctx->arena, name_node, ctx->source), url);
}
}
return;
}
if (strcmp(kind, "return_statement") != 0) {
return;
}
if (!state->enclosing_func_qn || state->enclosing_func_qn == ctx->module_qn) {
return;
}
if (ts_node_named_child_count(node) == 0) {
return;
}
const char *url = builder_route_url(ctx, ts_node_named_child(node, 0));
if (!url) {
return;
}
TSNode func = cbm_find_enclosing_func(node, ctx->language);
if (ts_node_is_null(func) || !builder_returns_only_urls(ctx, func)) {
return;
}
const char *name = strrchr(state->enclosing_func_qn, '.');
name = name ? name + 1 : state->enclosing_func_qn;
record_url_builder(ctx, name, url);
}
// --- 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;
/* On-stack first chunk of the key-path walk; wider mappings spill to the heap
* through cbm_walk_stack_reserve instead of dropping frames (#2388). */
#define YAML_WALK_STACK_INLINE CBM_SZ_256
/* Push block_mapping children of a block_node/block_mapping value onto the walk
* stack, growing it as needed. False when the stack could not grow (logged by
* cbm_walk_stack_reserve as extract.walk_stack_alloc_failed). */
static bool push_yaml_block_children(TSNode val, const char *path, yaml_walk_frame_t **stack,
int *cap, int *top, const yaml_walk_frame_t *inline_buf) {
uint32_t vnc = ts_node_named_child_count(val);
if (!cbm_walk_stack_reserve((void **)stack, cap, *top + (int)vnc, sizeof(yaml_walk_frame_t),
inline_buf, "yaml_key_paths")) {
return false;
}
for (int vi = (int)vnc - SKIP_ONE; vi >= 0; 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};
}
}
return true;
}
static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode root, const char *root_prefix) {
yaml_walk_frame_t inline_stack[YAML_WALK_STACK_INLINE];
yaml_walk_frame_t *stack = inline_stack;
int cap = YAML_WALK_STACK_INLINE;
int top = 0;
stack[top++] = (yaml_walk_frame_t){root, root_prefix};
bool grown = true;
while (grown && 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) {
grown = push_yaml_block_children(val, path, &stack, &cap, &top, inline_stack);
if (!grown) {
break;
}
continue;
}
emit_yaml_leaf_value(ctx, val, path);
}
}
cbm_walk_stack_release(stack, inline_stack);
}
/* ── 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);
}
/* On-stack first chunk of the binding scan; wider subtrees spill to the heap
* through cbm_walk_stack_reserve instead of dropping nodes (#2388). */
#define INFRA_SCAN_STACK_INLINE CBM_SZ_512
static void scan_yaml_for_infra_bindings(CBMExtractCtx *ctx, TSNode root) {
TSNode inline_stack[INFRA_SCAN_STACK_INLINE];
TSNode *stack = inline_stack;
int cap = INFRA_SCAN_STACK_INLINE;
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);
if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)nc, sizeof(TSNode),
inline_stack, "yaml_infra_bindings")) {
break;
}
for (int i = (int)nc - SKIP_ONE; i >= 0; i--) {
stack[top++] = ts_node_named_child(node, (uint32_t)i);
}
}
cbm_walk_stack_release(stack, inline_stack);
}
/* ── 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);
}
}
}
/* Extra nodes are grammar-declared trivia (most commonly comments). They still
* participate in cursor traversal and scope expiry, but none of the unified
* semantic handlers consumes their contents; documentation is collected by
* the definition/LSP passes. The grammar predicate covers every language
* without confusing structured syntax such as SQL COMMENT statements. */
static bool is_unified_trivia_node(TSNode node) {
return ts_node_is_extra(node);
}
static bool unified_node_text_equals(const CBMExtractCtx *ctx, TSNode node, const char *expected) {
if (ts_node_is_null(node) || !ctx->source || !expected) {
return false;
}
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
size_t len = strlen(expected);
return end >= start && (size_t)(end - start) == len &&
memcmp(ctx->source + start, expected, len) == 0;
}
/* An Elixir module attribute parses as `unary_operator(@, call(<attr>, args))`,
* so the typespec family reaches the unified walk in the shape real code has:
* `@spec foo(t) :: t` puts `foo(t)` in the same `call` node an invocation would
* occupy, and `@type entry :: String.t()` does the same to a type name. The
* walk then reads a declaration as code and mints reference edges from it.
*
* Measured on a fixture of three typespec lines (`@type state :: map()`, `@spec
* fetch(opts) :: state`, `@spec load(User.t()) :: :ok`) beside `def state`,
* `def opts` and a remote `MyApp.User.t/0`: seven edges, every one phantom and
* every one landing on a Function. CALLS onto each specified function;
* CALLS + USAGE + WRITES onto `state`, the WRITES asserting a mutation that
* does not exist; and USAGE onto `opts` and onto `MyApp.User.t/0`, because a
* bare or remote TYPE name resolves to the same-named FUNCTION. On a codebase
* whose convention is @spec on every public function, fan_in therefore never
* reaches 0 and "which exported functions nothing calls" is unanswerable.
*
* The whole subtree is skipped, not just the specified name. What that costs is
* not uniform across the six heads, and both halves were measured by indexing
* one fixture repo -- a declaring file per head plus the file each one reaches
* -- with the pristine binary and with this one:
*
* spec, type, typep, opaque. A type reference resolves onto a FUNCTION or
* onto nothing, never onto the type it names, because a type declaration
* mints no node. `@spec build(MyApp.User.t())` where that module's `t` is a
* `@type` produced no edge in either build, and `@type ext :: Ecto.Schema.t()`
* naming a module outside the repo produced none either. The references that
* do resolve land on a same-named Function, cross-file included -- `USAGE
* lib/decl_type.ex -> Function type_target @lib/remote_type.ex`, and the
* @typep and @opaque equivalents. A Function standing in for a Type is the
* pollution rather than a record of it, so nothing correct is lost here.
*
* callback, macrocallback. The declared name IS a function name, so it
* resolves onto an implementation elsewhere in the corpus: pristine yields
* `CALLS lib/decl_callback.ex -> Function callback_fun @lib/impl_callback.ex`
* and the @macrocallback equivalent, both cross-file, and this build yields
* neither. That edge does point at a real Behaviour-to-Impl relationship, and
* nothing replaces it -- `@behaviour MyBehaviour` mints no edge of its own,
* so after this change nothing in the graph links a behaviour to its
* implementations. It is deleted anyway because it is name resolution and not
* a behaviour model: on a fixture with one @callback and three same-named
* `handle_it/1` defs, the single edge landed on the one module that
* implements nothing, leaving both real implementors at fan_in 0. Recording
* that relationship correctly means minting it from `@behaviour`, which is a
* separate change and not a reason to keep an arbitrary edge.
*
* The other accepted cost: `unquote(...)` inside a typespec does execute at
* compile time, so `@type t :: unquote(build_type())` loses its genuine
* `build_type` call edge along with the phantoms.
*
* Only this family is skipped. Every other attribute VALUE is ordinary
* compile-time code -- `@timeout Application.compile_env(:app, :timeout)`
* really does call compile_env -- so those subtrees are walked exactly as
* before, and their attribute NAME is still emitted as a callee: `@behaviour
* GenServer`, `@timeout 5_000` and `@doc "x"` each still mint one phantom CALLS
* onto a same-named Function. That half of the defect class is untouched here
* and needs its own fix.
*
* This subtree skip is one of two mechanisms, and neither subsumes the other.
* It removes the phantom a typespec LINE mints; the def-head phantom is removed
* separately, by the calls walk recognising a `when`-wrapped head as a
* declaration (elixir_call_is_definition_role, extract_calls.c). A function
* carrying both an @spec and a `when` clause needs both: with only this one it
* keeps the def-head reference, with only the other it keeps the @spec one.
* `extract_elixir_typespec_attribute_is_not_code` pins that by asserting 0
* references onto such a function, so removing either mechanism fails it.
*
* Measured over forge-symphony-graph/lib (970 .ex files) by classifying the
* source line every CALLS/USAGE/WRITES edge records: a build without this skip
* anchors 4,565 CALLS edges on a typespec-attribute line, and this build
* anchors 0 -- with no USAGE or WRITES appearing in their place, which is what
* a relabel rather than a removal would look like. Total USAGE edges fall from
* 25,393 to 23,044 over the same corpus; they do not rise. */
static bool is_elixir_typespec_attribute(const CBMExtractCtx *ctx, TSNode node) {
static const char *const typespec_heads[] = {
"spec", "callback", "macrocallback", "type", "typep", "opaque", NULL};
if (ctx->language != CBM_LANG_ELIXIR || strcmp(ts_node_type(node), "unary_operator") != 0 ||
!unified_node_text_equals(ctx, ts_node_child_by_field_name(node, TS_FIELD("operator")),
"@")) {
return false;
}
TSNode operand = ts_node_child_by_field_name(node, TS_FIELD("operand"));
if (ts_node_is_null(operand) || strcmp(ts_node_type(operand), "call") != 0 ||
ts_node_child_count(operand) == 0) {
return false;
}
TSNode head = ts_node_child(operand, 0);
for (const char *const *name = typespec_heads; *name; name++) {
if (unified_node_text_equals(ctx, head, *name)) {
return true;
}
}
return false;
}
// 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;
}
// Some languages encode imports with the same generic AST node used by every
// ordinary invocation. Static node-kind membership is therefore not enough to
// make the whole subtree an import scope.
static bool is_actual_import_boundary(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
bool direct_import = spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types);
bool from_import = spec->import_from_types && cbm_kind_in_set(node, spec->import_from_types);
if (!direct_import && !from_import) {
return false;
}
const char *kind = ts_node_type(node);
TSNode head = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (ts_node_is_null(head)) {
head = ts_node_child_by_field_name(node, TS_FIELD("method"));
}
if (ts_node_is_null(head)) {
head = ts_node_child_by_field_name(node, TS_FIELD("command_name"));
}
if (ts_node_is_null(head)) {
head = ts_node_child_by_field_name(node, TS_FIELD("name"));
}
if (ts_node_is_null(head) && ts_node_named_child_count(node) > 0) {
head = ts_node_named_child(node, 0);
}
char *name = ts_node_is_null(head) ? NULL : cbm_node_text(ctx->arena, head, ctx->source);
switch (ctx->language) {
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
case CBM_LANG_ARKTS:
if (strcmp(kind, "export_statement") == 0) {
/* An export is an import CONTEXT only in its re-export forms:
* `export ... from 'mod'` (source field) or a bare specifier list
* `export { a, b }` with no declaration. An export OF a declaration
* must not put the declaration's body behind inside_import — the
* old kind-blacklist (is_export_of_declaration) missed the TS-only
* forms (ambient_declaration, function_signature,
* module_declaration), so declare-heavy code (.d.ts, baselines,
* `export namespace`) ran whole subtrees as import context:
* suppressed usages plus per-identifier ancestor walks. Positive
* detection replaces the blacklist. */
if (!ts_node_is_null(ts_node_child_by_field_name(node, TS_FIELD("source")))) {
return true;
}
return !ts_node_is_null(cbm_find_child_by_kind(node, "export_clause")) &&
ts_node_is_null(ts_node_child_by_field_name(node, TS_FIELD("declaration")));
}
return true; /* import_statement / import / require / extends: unchanged */
case CBM_LANG_CSHARP:
/* cs_import_types also lists namespace_declaration (so the import pass
* can map namespace names) and using_statement (C#'s RAII block, a
* grammar-name collision with using_directive). Neither is an import
* CONTEXT: treating them as one put every namespaced C# file's whole
* body behind inside_import, which both suppressed ordinary usage
* extraction there and sent every identifier through the ancestor-
* walking import-binding check — 92% of extract time on wide files
* (dotnet/runtime JIT torture tests, 490 s for one 147 KB file). Only
* the using DIRECTIVE opens an import scope. */
return strcmp(kind, "using_directive") == 0 ||
strcmp(kind, "namespace_use_declaration") == 0;
case CBM_LANG_ELIXIR:
return strcmp(kind, "call") != 0 ||
(name && (strcmp(name, "import") == 0 || strcmp(name, "alias") == 0 ||
strcmp(name, "require") == 0 || strcmp(name, "use") == 0));
case CBM_LANG_LUA:
return strcmp(kind, "function_call") != 0 || (name && strcmp(name, "require") == 0);
case CBM_LANG_RUBY:
return strcmp(kind, "call") != 0 ||
(name && (strcmp(name, "require") == 0 || strcmp(name, "require_relative") == 0));
case CBM_LANG_BASH:
return strcmp(kind, "command") != 0 ||
(name && (strcmp(name, "source") == 0 || strcmp(name, ".") == 0));
case CBM_LANG_R:
if (strcmp(kind, "call") != 0 || !name) {
return strcmp(kind, "call") != 0;
}
return strcmp(name, "library") == 0 || strcmp(name, "require") == 0 ||
strcmp(name, "requireNamespace") == 0 || strcmp(name, "loadNamespace") == 0 ||
strcmp(name, "source") == 0 || strcmp(name, "box::use") == 0;
case CBM_LANG_ZIG: {
if (strcmp(kind, "builtin_function") != 0) {
return true;
}
char *text = cbm_node_text(ctx->arena, node, ctx->source);
return text && (strncmp(text, "@import", sizeof("@import") - 1) == 0 ||
strncmp(text, "@cImport", sizeof("@cImport") - 1) == 0);
}
case CBM_LANG_SCSS:
/* `@include` is an executable mixin invocation, not a module import. */
return strcmp(kind, "include_statement") != 0;
default:
return true;
}
}
/* NASM labels and instructions are flat siblings. For an actual instruction,
* find the nearest preceding label at the first ancestor level that has one.
* This is structural scope recovery; the instruction mnemonic is not used. */
static TSNode nasm_wrapped_label(TSNode node, int remaining_depth) {
if (ts_node_is_null(node) || remaining_depth < 0) {
return (TSNode){0};
}
if (strcmp(ts_node_type(node), "label") == 0) {
return node;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "instruction") != 0 && strcmp(kind, "source_line") != 0) {
return (TSNode){0};
}
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = 0; i < count; i++) {
TSNode label = nasm_wrapped_label(ts_node_named_child(node, i), remaining_depth - 1);
if (!ts_node_is_null(label)) {
return label;
}
}
return (TSNode){0};
}
static TSNode nasm_preceding_label(TSNode node) {
TSNode current = node;
for (int level = 0; level < 4 && !ts_node_is_null(current); level++) {
TSNode previous = ts_node_prev_named_sibling(current);
while (!ts_node_is_null(previous)) {
TSNode label = nasm_wrapped_label(previous, 2);
if (!ts_node_is_null(label)) {
return label;
}
previous = ts_node_prev_named_sibling(previous);
}
TSNode parent = ts_node_parent(current);
if (ts_node_is_null(parent) || strcmp(ts_node_type(current), "source_file") == 0) {
break;
}
current = parent;
}
return (TSNode){0};
}
/* Traditional ObjectScript routines are flat: a tag statement is followed by
* sibling command statements until the next tag. Re-anchor each top-level
* command statement to its nearest preceding tag, just as NASM instructions
* are re-anchored to flat labels. Braced `procedure` nodes already provide a
* real lexical boundary and deliberately bypass this recovery path. */
static TSNode objectscript_routine_statement_tag(TSNode statement) {
TSNode tag_statement = cbm_find_child_by_kind(statement, "tag_statement");
return ts_node_is_null(tag_statement) ? (TSNode){0}
: cbm_find_child_by_kind(tag_statement, "tag");
}
static TSNode objectscript_routine_preceding_tag(TSNode node) {
if (strcmp(ts_node_type(node), "statement") != 0) {
return (TSNode){0};
}
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "source_file") != 0 ||
!ts_node_is_null(cbm_find_child_by_kind(node, "tag_statement")) ||
!ts_node_is_null(cbm_find_child_by_kind(node, "procedure"))) {
return (TSNode){0};
}
for (TSNode previous = ts_node_prev_named_sibling(node); !ts_node_is_null(previous);
previous = ts_node_prev_named_sibling(previous)) {
TSNode tag = objectscript_routine_statement_tag(previous);
if (!ts_node_is_null(tag)) {
return tag;
}
}
return (TSNode){0};
}
static bool push_pre_node_scope(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state, uint32_t depth) {
for (int i = 0; i < state->scope_top; i++) {
if (state->scopes[i].kind == SCOPE_FUNC) {
return false;
}
}
TSNode label = {0};
if (ctx->language == CBM_LANG_NASM && strcmp(ts_node_type(node), "actual_instruction") == 0) {
label = nasm_preceding_label(node);
} else if (ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
if (strcmp(ts_node_type(node), "tag_statement") == 0) {
/* A parameterized flat tag owns its header as well as the following
* sibling command statements. Anchor before descending so its
* parameter_list records lexical bindings under the same tag QN. */
label = cbm_find_child_by_kind(node, "tag");
} else {
label = objectscript_routine_preceding_tag(node);
}
}
if (ts_node_is_null(label)) {
return false;
}
const char *fqn = compute_func_qn(ctx, label, spec, state);
if (!fqn) {
return false;
}
uint32_t anchor_start = ts_node_start_byte(label);
uint32_t anchor_end = ts_node_end_byte(label);
bool same_flat_callable = state->flat_function_scope_id != 0 &&
state->flat_anchor_start_byte == anchor_start &&
state->flat_anchor_end_byte == anchor_end &&
state->flat_function_qn && strcmp(state->flat_function_qn, fqn) == 0;
bool pushed = same_flat_callable
? push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
state->flat_function_scope_id, node)
: push_function_scope(state, depth, fqn, node);
if (pushed && !same_flat_callable) {
state->flat_function_scope_id = state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id;
state->flat_anchor_start_byte = anchor_start;
state->flat_anchor_end_byte = anchor_end;
state->flat_function_qn = fqn;
}
return pushed;
}
static bool unified_kind_in_set(const char *kind, const char *const *set) {
for (int i = 0; kind && set && set[i]; i++) {
if (strcmp(kind, set[i]) == 0) {
return true;
}
}
return false;
}
static bool lexical_boundary_kind(TSNode node, CBMLexicalScopeKind *out_kind) {
const char *kind = ts_node_type(node);
static const char *const block_kinds[] = {"block",
"statement_block",
"compound_statement",
"switch_body",
"catch_clause",
"except_clause",
"finally_clause",
"for_statement",
"for_in_statement",
"for_of_statement",
"enhanced_for_statement",
"foreach_statement",
"foreach_clause",
"loop_expression",
"match_block",
"case_block",
"script_block",
"do_block",
NULL};
static const char *const comprehension_kinds[] = {
"list_comprehension", "set_comprehension", "dictionary_comprehension",
"generator_expression", "comprehension_expression", NULL};
static const char *const anonymous_function_kinds[] = {"lambda",
"lambda_expression",
"anonymous_function",
"anonymous_function_creation_expression",
"anonymous_method_expression",
"arrow_function",
"closure_expression",
"func_literal",
"function_expression",
"function_literal",
NULL};
/* Rust inline modules own independent item/import namespaces. Without a
* concrete module scope, `mod inner { use ... as x; }` leaks x into every
* sibling function in the file. */
if (strcmp(kind, "mod_item") == 0) {
*out_kind = CBM_LEXICAL_SCOPE_MODULE;
return true;
}
if (unified_kind_in_set(kind, comprehension_kinds)) {
*out_kind = CBM_LEXICAL_SCOPE_COMPREHENSION;
return true;
}
if (unified_kind_in_set(kind, anonymous_function_kinds)) {
*out_kind = CBM_LEXICAL_SCOPE_FUNCTION;
return true;
}
if (unified_kind_in_set(kind, block_kinds)) {
*out_kind = CBM_LEXICAL_SCOPE_BLOCK;
return true;
}
return false;
}
static bool node_already_has_lexical_scope(const WalkState *state, TSNode node) {
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
for (int i = state->scope_top - 1; i >= 0; i--) {
const CBMLexicalScope *scope =
lexical_scope_by_id(state, state->scopes[i].lexical_scope_id);
if (scope && scope->start_byte == start && scope->end_byte == end) {
return true;
}
}
return false;
}
static void push_lexical_boundary(TSNode node, WalkState *state, uint32_t depth) {
CBMLexicalScopeKind kind;
if (!node_already_has_lexical_scope(state, node) && lexical_boundary_kind(node, &kind)) {
(void)push_lexical_scope(state, SCOPE_LEXICAL, depth, NULL, node, kind);
}
}
// 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,
const CBMInvocationDescriptor *invocation) {
const char *configured_body_qn = NULL;
if (ctx->language == CBM_LANG_C && strcmp(ts_node_type(node), "compound_statement") == 0)
configured_body_qn = cbm_test_definition_qn(ctx, node);
if (configured_body_qn) {
(void)push_function_scope(state, depth, configured_body_qn, node);
} else 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_function_scope(state, depth, fqn, node)) {
const char *node_kind = ts_node_type(node);
bool split_signature = (ctx->language == CBM_LANG_DART &&
(strcmp(node_kind, "function_signature") == 0 ||
strcmp(node_kind, "method_signature") == 0)) ||
(ctx->language == CBM_LANG_LLVM_IR &&
strcmp(node_kind, "function_header") == 0);
if (split_signature) {
state->split_function_scope_id =
state->scopes[state->scope_top - SKIP_ONE].lexical_scope_id;
state->split_signature_start_byte = ts_node_start_byte(node);
state->split_signature_end_byte = ts_node_end_byte(node);
state->split_function_qn = fqn;
}
// ObjectScript: entering a method resets local var types (keeping
// class-level property types) and seeds the declared parameter types.
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
state->os_type_map.count = state->os_type_map.class_base_count;
TSNode mdef = cbm_find_child_by_kind(node, "method_definition");
if (ts_node_is_null(mdef)) {
mdef = node;
}
TSNode args_node = cbm_find_child_by_kind(mdef, "arguments");
if (!ts_node_is_null(args_node)) {
for (uint32_t ai = 0; ai < ts_node_named_child_count(args_node); ai++) {
TSNode arg = ts_node_named_child(args_node, ai);
if (strcmp(ts_node_type(arg), "argument") != 0) {
continue;
}
TSNode param_name_node = {0};
TSNode type_node = {0};
for (uint32_t pi = 0; pi < ts_node_named_child_count(arg); pi++) {
TSNode pchild = ts_node_named_child(arg, pi);
const char *pk = ts_node_type(pchild);
if (strcmp(pk, "method_arg") == 0) {
param_name_node = pchild;
} else if (strcmp(pk, "return_type") == 0) {
type_node = cbm_find_child_by_kind(pchild, "typename");
}
}
if (!ts_node_is_null(param_name_node) && !ts_node_is_null(type_node)) {
TSNode lvn = cbm_find_child_by_kind(param_name_node, "expr_atom");
if (ts_node_is_null(lvn)) {
lvn = param_name_node;
}
char *pname = cbm_node_text(ctx->arena, lvn, ctx->source);
char *ptype = cbm_node_text(ctx->arena, type_node, ctx->source);
if (pname && pname[0] && ptype && ptype[0]) {
os_type_map_add(&state->os_type_map, pname, ptype);
}
}
}
}
}
}
}
} else if (cbm_is_namespace_scope_kind(ctx->language, ts_node_type(node))) {
const char *namespace_qn = compute_class_qn(ctx, node, state);
if (namespace_qn) {
push_lexical_scope(state, SCOPE_NAMESPACE, depth, namespace_qn, node,
CBM_LEXICAL_SCOPE_MODULE);
}
} 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_lexical_scope(state, SCOPE_CLASS, depth, cqn, node, CBM_LEXICAL_SCOPE_CLASS);
// ObjectScript: a new class clears the type map entirely.
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
state->os_type_map.count = 0;
state->os_type_map.class_base_count = 0;
}
}
} else if (ctx->language == CBM_LANG_NIX && strcmp(ts_node_type(node), "binding") == 0 &&
cbm_nix_binding_is_attrset_scope(node)) {
/* Nix: a binding whose value is an attribute set is a named scope, exactly
* as is_namespace_scope_kind treats it on the def side. Pushing it here is
* what makes an in-body call source to `proj.file.setA.fn` rather than the
* bare `proj.file.fn` that no node carries once defs are attrpath-qualified. */
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_lexical_scope(state, SCOPE_CLASS, depth, tqn, node, CBM_LEXICAL_SCOPE_CLASS);
}
}
} 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) {
bool exact_pending =
state->split_function_scope_id != 0 && state->split_function_qn &&
strcmp(state->split_function_qn, fqn) == 0 &&
state->split_signature_start_byte == ts_node_start_byte(prev) &&
state->split_signature_end_byte == ts_node_end_byte(prev);
if (exact_pending) {
push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
state->split_function_scope_id, node);
} else {
push_function_scope(state, depth, fqn, node);
}
state->split_function_scope_id = 0;
state->split_function_qn = NULL;
}
}
} else if (ctx->language == CBM_LANG_LLVM_IR &&
strcmp(ts_node_type(node), "function_body") == 0) {
/* LLVM's function_header and function_body are siblings under
* fn_define. Re-anchor the already-extracted header definition across
* its body so instruction_call nodes inherit that routine. */
TSNode previous = ts_node_prev_sibling(node);
while (!ts_node_is_null(previous) &&
strcmp(ts_node_type(previous), "function_header") != 0) {
previous = ts_node_prev_sibling(previous);
}
if (!ts_node_is_null(previous)) {
const char *fqn = compute_func_qn(ctx, previous, spec, state);
if (fqn) {
bool exact_pending =
state->split_function_scope_id != 0 && state->split_function_qn &&
strcmp(state->split_function_qn, fqn) == 0 &&
state->split_signature_start_byte == ts_node_start_byte(previous) &&
state->split_signature_end_byte == ts_node_end_byte(previous);
if (exact_pending) {
push_existing_lexical_scope(state, SCOPE_FUNC, depth, fqn,
state->split_function_scope_id, node);
} else {
push_function_scope(state, depth, fqn, node);
}
state->split_function_scope_id = 0;
state->split_function_qn = NULL;
}
}
}
push_lexical_boundary(node, state, depth);
py_bind_scope_parameters(ctx, node, state);
push_call_scope(state, depth, invocation);
if (is_actual_import_boundary(ctx, node, spec) && !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);
TSTreeCursor occurrence_cursor = ts_tree_cursor_copy(&cursor);
WalkState state;
memset(&state, 0, sizeof(state));
state.current_cursor = &cursor;
state.occurrence_cursor = &occurrence_cursor;
state.arena = ctx->arena;
state.language = ctx->language;
state.scopes = state.inline_scopes;
state.scope_capacity = MAX_SCOPES;
state.lexical_scopes = state.inline_lexical_scopes;
state.lexical_scope_capacity = INLINE_LEXICAL_SCOPES;
state.py_param_slots = state.inline_py_param_slots;
state.py_param_slot_capacity = INLINE_PY_PARAM_SLOTS;
state.py_param_slot_used = 0;
memset(state.inline_py_param_slots, 0, sizeof(state.inline_py_param_slots));
state.py_param_stack = state.inline_py_param_stack;
state.py_param_stack_capacity = INLINE_PY_PARAM_STACK;
state.py_param_stack_count = 0;
state.py_param_tracking_failed = false;
state.usage_start_index = ctx->result->usages.count;
state.root_lexical_scope_id = add_lexical_scope(&state, ctx->root, CBM_LEXICAL_SCOPE_MODULE);
/* Base walk-state tuple (previously established by the first
* recompute_state call): module scope, nothing else active. */
state.enclosing_func_qn = ctx->module_qn;
state.enclosing_class_qn = NULL;
state.enclosing_class_node = (TSNode){0};
state.invocation_kind = CBM_INVOCATION_NONE;
state.callee_expr = (TSNode){0};
state.callee_leaf = (TSNode){0};
state.inside_import = false;
state.loop_depth = 0;
state.branch_depth = 0;
uint32_t depth = 0;
uint32_t visited = 0;
#ifdef CBM_ENABLE_TEST_SEAMS
/* CBM_TEST_WALK_BUDGET_NODES=<n>: spend the budget after n nodes. Production
* now counts nodes too, so this seam only shortens the budget; it no longer
* has to stand in for a mechanism of its own. */
uint32_t budget_nodes = ctx->walk_budget_nodes;
{
const char *seam = getenv("CBM_TEST_WALK_BUDGET_NODES");
if (seam && seam[0]) {
budget_nodes = (uint32_t)strtoul(seam, NULL, 10);
}
}
#else
const uint32_t budget_nodes = ctx->walk_budget_nodes;
#endif
/* A Terraform / OpenTofu module file's references resolve inside its
* module directory (pass_terraform.c); a call or usage by name here would
* meet same-named blocks of any module and a builtin function's name. */
const char *ext = ctx->rel_path ? strrchr(ctx->rel_path, '.') : NULL;
bool tf_module = ctx->language == CBM_LANG_HCL && ext &&
(strcmp(ext, ".tf") == 0 || strcmp(ext, ".tofu") == 0);
for (;;) {
TSNode node = ts_tree_cursor_current_node(&cursor);
visited++;
/* The budget is spent in nodes, so the walk of a given file always ends
* on the same node — on any machine, under any load. A CPU deadline
* here made the graph differ between two runs on one machine; see
* CBM_WALK_MAX_NODES_DEFAULT (cbm.c). */
if (budget_nodes != 0 && visited > budget_nodes) {
ctx->walk_budget_exhausted = true;
break;
}
bool trivia = is_unified_trivia_node(node);
bool typespec = is_elixir_typespec_attribute(ctx, node);
if (!trivia && !typespec) {
/* Trivia consumes no semantic state. Scope expiry may be deferred
* until the next code-bearing node; pop restores the displaced
* tuple and push applies the new frame's effect, both O(1) --
* the old whole-stack recompute here was O(depth) per node and
* quadratic on the deep-nesting torture tests. */
pop_expired_scopes(&state, depth);
(void)push_pre_node_scope(ctx, node, spec, &state, depth);
handle_string_constants(ctx, node, &state);
handle_objectscript_type_map(ctx, node, &state);
handle_url_builders(ctx, node, &state);
CBMInvocationDescriptor invocation =
tf_module ? (CBMInvocationDescriptor){0} : handle_calls(ctx, node, spec, &state);
if (!tf_module) {
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, &invocation);
}
/* Lexer-terminal trivia has no semantic work and no descendants. Avoid
* asking the cursor to construct a child iterator for every one of
* hundreds of thousands of flat comment siblings. Structured extras
* still descend normally. */
if (!typespec && (!trivia || ts_node_child_count(node) > 0) &&
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;
}
}
cbm_finalize_lexical_usages(ctx, &state);
ctx->walk_nodes_visited = visited;
ts_tree_cursor_delete(&occurrence_cursor);
ts_tree_cursor_delete(&cursor);
}