mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
2936 lines
128 KiB
C
2936 lines
128 KiB
C
#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);
|
||
}
|