Files
deusdata--codebase-memory-mcp/internal/cbm/extract_unified.c
T
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

1240 lines
51 KiB
C
Raw Blame History

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