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