#include "cbm.h" #include "arena.h" // CBMArena, cbm_arena_strdup/strndup/sprintf #include "helpers.h" #include "lang_specs.h" // CBMLangSpec, CBMEmbeddedLangSpec, cbm_lang_spec, cbm_ts_language #include "tree_sitter/api.h" // TSNode, ts_node_* #include "foundation/constants.h" #include "extract_node_stack.h" #include // uint32_t #include #include /* Local constants for magic number elimination. */ enum { USE_PREFIX_LEN = 4, /* strlen("use ") */ MIN_WOLFRAM_CHILDREN = 2, SECOND_IDX = 1, }; // Field name length for ts_node_child_by_field_name() calls. #define FIELD_LEN_MODULE_NAME 11 // strlen("module_name") // Forward declarations static void parse_go_imports(CBMExtractCtx *ctx); static void parse_python_imports(CBMExtractCtx *ctx); static void parse_es_imports(CBMExtractCtx *ctx); static void parse_java_imports(CBMExtractCtx *ctx); static void parse_rust_imports(CBMExtractCtx *ctx); static void parse_c_imports(CBMExtractCtx *ctx); static void parse_ruby_imports(CBMExtractCtx *ctx); static void parse_lua_imports(CBMExtractCtx *ctx); static void parse_r_imports(CBMExtractCtx *ctx); static void parse_kotlin_imports(CBMExtractCtx *ctx); static void parse_dart_imports(CBMExtractCtx *ctx); static void parse_haskell_imports(CBMExtractCtx *ctx); static void parse_zig_imports(CBMExtractCtx *ctx); static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type); static void parse_wolfram_imports(CBMExtractCtx *ctx); static void parse_php_imports(CBMExtractCtx *ctx); static void parse_csharp_imports(CBMExtractCtx *ctx); static void parse_spec_imports(CBMExtractCtx *ctx); static void parse_hare_imports(CBMExtractCtx *ctx); static void parse_pascal_imports(CBMExtractCtx *ctx); static void parse_powershell_imports(CBMExtractCtx *ctx); static void parse_lisp_imports(CBMExtractCtx *ctx); static void parse_starlark_imports(CBMExtractCtx *ctx); static void parse_tcl_imports(CBMExtractCtx *ctx); static void parse_teal_imports(CBMExtractCtx *ctx); static void parse_zsh_imports(CBMExtractCtx *ctx); static void parse_css_imports(CBMExtractCtx *ctx); static void parse_html_imports(CBMExtractCtx *ctx); static void parse_cmake_imports(CBMExtractCtx *ctx); static void parse_bitbake_imports(CBMExtractCtx *ctx); static void parse_kconfig_imports(CBMExtractCtx *ctx); static void parse_gn_imports(CBMExtractCtx *ctx); static void parse_just_imports(CBMExtractCtx *ctx); static void parse_nix_imports(CBMExtractCtx *ctx); static void parse_jsonnet_imports(CBMExtractCtx *ctx); static void parse_pkl_imports(CBMExtractCtx *ctx); static void parse_nickel_imports(CBMExtractCtx *ctx); static void parse_thrift_imports(CBMExtractCtx *ctx); static void parse_capnp_imports(CBMExtractCtx *ctx); static void parse_dlang_imports(CBMExtractCtx *ctx); static void parse_tablegen_imports(CBMExtractCtx *ctx); static void parse_crystal_imports(CBMExtractCtx *ctx); static void parse_fsharp_imports(CBMExtractCtx *ctx); static void parse_ada_imports(CBMExtractCtx *ctx); static void parse_elm_imports(CBMExtractCtx *ctx); static void parse_move_imports(CBMExtractCtx *ctx); static void parse_smali_imports(CBMExtractCtx *ctx); static void parse_tlaplus_imports(CBMExtractCtx *ctx); static void parse_vhdl_imports(CBMExtractCtx *ctx); static void parse_wit_imports(CBMExtractCtx *ctx); static void parse_smithy_imports(CBMExtractCtx *ctx); static void parse_hyprlang_imports(CBMExtractCtx *ctx); // Helper: strip quotes from a string literal static char *strip_quotes(CBMArena *a, const char *s) { if (!s) { return NULL; } size_t len = strlen(s); if (len >= CBM_QUOTE_PAIR && (s[0] == '"' || s[0] == '\'') && s[len - SKIP_ONE] == s[0]) { return cbm_arena_strndup(a, s + SKIP_ONE, len - PAIR_LEN); } return cbm_arena_strdup(a, s); } // Helper: get last path component as local name. // Recognizes every separator used across the supported import syntaxes: // '/' (Go / TS / JS paths), '.' (Java / Kotlin / C# / Python dotted names), // '::' (Rust / C++ scope), '\\' (PHP namespaces). The last separator of any // kind wins, so "std::collections::HashMap" → "HashMap" and // "App\\Http\\Controller" → "Controller". static const char *path_last(CBMArena *a, const char *path) { if (!path) { return NULL; } const char *last_sep = NULL; for (const char *p = path; *p; p++) { if (*p == '/' || *p == '.' || *p == ':' || *p == '\\') { last_sep = p; } } if (last_sep) { return cbm_arena_strdup(a, last_sep + SKIP_ONE); } return path; } // --- Go imports --- // import_declaration -> import_spec_list -> import_spec -> (name, path) // Parse a single Go import_spec node. static void parse_go_import_spec(CBMExtractCtx *ctx, TSNode spec) { CBMArena *a = ctx->arena; TSNode path_node = ts_node_child_by_field_name(spec, TS_FIELD("path")); if (ts_node_is_null(path_node)) { return; } char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); if (!path || !path[0]) { return; } TSNode name_node = ts_node_child_by_field_name(spec, TS_FIELD("name")); const char *local_name = !ts_node_is_null(name_node) ? cbm_node_text(a, name_node, ctx->source) : path_last(a, path); CBMImport imp = {.local_name = local_name, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } static void parse_go_imports(CBMExtractCtx *ctx) { TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode decl = ts_tree_cursor_current_node(&cursor); if (strcmp(ts_node_type(decl), "import_declaration") != 0) { continue; } uint32_t dc = ts_node_child_count(decl); for (uint32_t j = 0; j < dc; j++) { TSNode child = ts_node_child(decl, j); const char *ck = ts_node_type(child); if (strcmp(ck, "import_spec") == 0) { parse_go_import_spec(ctx, child); } else if (strcmp(ck, "import_spec_list") == 0) { uint32_t sc = ts_node_child_count(child); for (uint32_t k = 0; k < sc; k++) { TSNode spec = ts_node_child(child, k); if (strcmp(ts_node_type(spec), "import_spec") == 0) { parse_go_import_spec(ctx, spec); } } } } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Python imports --- // import_statement: import X, import X as Y // import_from_statement: from X import Y, from X import Y as Z // Emit a Python aliased_import (import X as Y / from X import Y as Z). static void emit_py_aliased_import(CBMExtractCtx *ctx, TSNode child, const char *mod_prefix) { CBMArena *a = ctx->arena; TSNode mod_node = ts_node_child_by_field_name(child, TS_FIELD("name")); TSNode alias_node = ts_node_child_by_field_name(child, TS_FIELD("alias")); if (ts_node_is_null(mod_node)) { return; } char *name = cbm_node_text(a, mod_node, ctx->source); if (!name || !name[0]) { return; } const char *local = !ts_node_is_null(alias_node) ? cbm_node_text(a, alias_node, ctx->source) : path_last(a, name); const char *full = mod_prefix ? cbm_arena_sprintf(a, "%s.%s", mod_prefix, name) : name; CBMImport imp = {.local_name = local, .module_path = full}; cbm_imports_push(&ctx->result->imports, a, imp); } // Process a single Python import_statement node (import X, import X as Y). static void process_py_import_stmt(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)) { uint32_t nc = ts_node_child_count(node); for (uint32_t j = 0; j < nc; j++) { TSNode child = ts_node_child(node, j); const char *ck = ts_node_type(child); if (strcmp(ck, "dotted_name") == 0 || strcmp(ck, "identifier") == 0) { char *mod = cbm_node_text(a, child, ctx->source); if (mod && mod[0]) { CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod}; cbm_imports_push(&ctx->result->imports, a, imp); } } else if (strcmp(ck, "aliased_import") == 0) { emit_py_aliased_import(ctx, child, NULL); } } } else if (strcmp(ts_node_type(name_node), "aliased_import") == 0) { /* `import util as u` — the import_statement's `name` field points at the * aliased_import; extract its real module name (not "util as u"). */ emit_py_aliased_import(ctx, name_node, NULL); } else { char *mod = cbm_node_text(a, name_node, ctx->source); if (mod && mod[0]) { CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod}; cbm_imports_push(&ctx->result->imports, a, imp); } } } // Resolve the module_name node for a Python import_from_statement. static TSNode resolve_py_module_node(TSNode node) { TSNode module_node = ts_node_child_by_field_name(node, "module_name", FIELD_LEN_MODULE_NAME); if (ts_node_is_null(module_node)) { uint32_t nc = ts_node_child_count(node); for (uint32_t j = 0; j < nc; j++) { TSNode c = ts_node_child(node, j); if (strcmp(ts_node_type(c), "dotted_name") == 0 || strcmp(ts_node_type(c), "relative_import") == 0) { return c; } } } return module_node; } // Emit a Python import-from name child (identifier/dotted_name). static void emit_py_import_from_name(CBMExtractCtx *ctx, TSNode child, const char *mod_path) { CBMArena *a = ctx->arena; char *name = cbm_node_text(a, child, ctx->source); if (name && name[0]) { const char *full = mod_path ? cbm_arena_sprintf(a, "%s.%s", mod_path, name) : name; CBMImport imp = {.local_name = name, .module_path = full}; cbm_imports_push(&ctx->result->imports, a, imp); } } // Process a single Python import_from_statement node (from X import Y [as Z]). static void process_py_import_from(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; // `from __future__ import annotations` is a dedicated node type whose first // child is the literal `__future__` keyword (an identifier, not a // dotted_name). Emit a single import for `__future__` and return. if (strcmp(ts_node_type(node), "future_import_statement") == 0) { CBMImport imp = {.local_name = cbm_arena_strdup(a, "__future__"), .module_path = cbm_arena_strdup(a, "__future__")}; cbm_imports_push(&ctx->result->imports, a, imp); return; } TSNode module_node = resolve_py_module_node(node); char *mod_path = ts_node_is_null(module_node) ? NULL : cbm_node_text(a, module_node, ctx->source); uint32_t nc = ts_node_child_count(node); bool emitted = false; for (uint32_t j = 0; j < nc; j++) { TSNode child = ts_node_child(node, j); const char *ck = ts_node_type(child); if (strcmp(ck, "identifier") == 0 || strcmp(ck, "dotted_name") == 0) { if (!ts_node_is_null(module_node) && ts_node_start_byte(child) == ts_node_start_byte(module_node)) { continue; } emit_py_import_from_name(ctx, child, mod_path); emitted = true; } else if (strcmp(ck, "aliased_import") == 0) { emit_py_aliased_import(ctx, child, mod_path); emitted = true; } else if (strcmp(ck, "wildcard_import") == 0) { // `from os.path import *` — the module itself is the import. if (mod_path && mod_path[0]) { CBMImport imp = {.local_name = path_last(a, mod_path), .module_path = mod_path}; cbm_imports_push(&ctx->result->imports, a, imp); emitted = true; } } } // Defensive: a from-import with a module but no recognized name child // (grammar variant) still records the module as an import. if (!emitted && mod_path && mod_path[0]) { CBMImport imp = {.local_name = path_last(a, mod_path), .module_path = mod_path}; cbm_imports_push(&ctx->result->imports, a, imp); } } static void parse_python_imports(CBMExtractCtx *ctx) { TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); if (strcmp(kind, "import_statement") == 0) { process_py_import_stmt(ctx, node); } else if (strcmp(kind, "import_from_statement") == 0 || strcmp(kind, "future_import_statement") == 0) { // `from __future__ import annotations` is a distinct node type in // tree-sitter-python but has the same shape (module + name list). process_py_import_from(ctx, node); } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- ES module imports (JS/TS/TSX) --- // import X from "Y"; import {A, B} from "Y"; import * as X from "Y" // const X = require("Y") // Find the source string node in an ES import_statement. static TSNode find_es_source_node(TSNode node) { TSNode source_node = ts_node_child_by_field_name(node, TS_FIELD("source")); if (ts_node_is_null(source_node)) { uint32_t nc = ts_node_child_count(node); for (int j = (int)nc - SKIP_ONE; j >= 0; j--) { TSNode c = ts_node_child(node, (uint32_t)j); const char *ck = ts_node_type(c); if (strcmp(ck, "string") == 0 || strcmp(ck, "string_literal") == 0) { return c; } } } return source_node; } // Process named_imports: import {A, B as C} from "path". static bool process_named_imports(CBMExtractCtx *ctx, TSNode sub, const char *path) { CBMArena *a = ctx->arena; bool found = false; uint32_t nc2 = ts_node_child_count(sub); for (uint32_t m = 0; m < nc2; m++) { TSNode imp_spec = ts_node_child(sub, m); if (strcmp(ts_node_type(imp_spec), "import_specifier") != 0) { continue; } TSNode local = ts_node_child_by_field_name(imp_spec, TS_FIELD("alias")); TSNode orig = ts_node_child_by_field_name(imp_spec, TS_FIELD("name")); if (ts_node_is_null(orig) && ts_node_child_count(imp_spec) > 0) { orig = ts_node_child(imp_spec, 0); } if (!ts_node_is_null(orig)) { char *local_name = !ts_node_is_null(local) ? cbm_node_text(a, local, ctx->source) : cbm_node_text(a, orig, ctx->source); CBMImport imp = {.local_name = local_name, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); found = true; } } return found; } // Process an import_clause node: default, namespace, and named imports. static bool process_import_clause(CBMExtractCtx *ctx, TSNode clause, const char *path) { CBMArena *a = ctx->arena; bool found = false; uint32_t cc = ts_node_child_count(clause); for (uint32_t k = 0; k < cc; k++) { TSNode sub = ts_node_child(clause, k); const char *sk = ts_node_type(sub); if (strcmp(sk, "identifier") == 0) { char *name = cbm_node_text(a, sub, ctx->source); CBMImport imp = {.local_name = name, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); found = true; } else if (strcmp(sk, "namespace_import") == 0) { TSNode as_name = ts_node_child_by_field_name(sub, TS_FIELD("name")); if (ts_node_is_null(as_name) && ts_node_child_count(sub) > 0) { as_name = ts_node_child(sub, ts_node_child_count(sub) - SKIP_ONE); } if (!ts_node_is_null(as_name)) { char *name = cbm_node_text(a, as_name, ctx->source); CBMImport imp = {.local_name = name, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); found = true; } } else if (strcmp(sk, "named_imports") == 0) { if (process_named_imports(ctx, sub, path)) { found = true; } } } return found; } /* Process a single ES import_statement node. Returns true if fully handled. */ static bool process_es_import_statement(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; TSNode source_node = find_es_source_node(node); if (ts_node_is_null(source_node)) { return false; } char *path = strip_quotes(a, cbm_node_text(a, source_node, ctx->source)); if (!path || !path[0]) { return false; } uint32_t nc = ts_node_child_count(node); bool found = false; for (uint32_t j = 0; j < nc; j++) { TSNode child = ts_node_child(node, j); const char *ck = ts_node_type(child); if (strcmp(ck, "identifier") == 0) { char *name = cbm_node_text(a, child, ctx->source); CBMImport imp = {.local_name = name, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); found = true; } else if (strcmp(ck, "import_clause") == 0) { if (process_import_clause(ctx, child, path)) { found = true; } } } if (!found) { CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } return true; } /* Handle CommonJS `require("path")` call_expression nodes. The local name * is derived from the enclosing variable_declarator / assignment when * possible (so `const foo = require('./foo')` emits local_name="foo"), * otherwise falls back to the last path component. */ static bool process_commonjs_require(CBMExtractCtx *ctx, TSNode call) { CBMArena *a = ctx->arena; if (ts_node_child_count(call) < MIN_WOLFRAM_CHILDREN) { return false; } /* Callee must be the identifier "require". */ TSNode fn = ts_node_child_by_field_name(call, TS_FIELD("function")); if (ts_node_is_null(fn)) { fn = ts_node_child(call, 0); } if (ts_node_is_null(fn)) { return false; } const char *fn_kind = ts_node_type(fn); if (strcmp(fn_kind, "identifier") != 0) { return false; } char *fn_name = cbm_node_text(a, fn, ctx->source); if (!fn_name || strcmp(fn_name, "require") != 0) { return false; } /* First string literal child of the argument list is the module path. */ TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments")); if (ts_node_is_null(args)) { return false; } uint32_t argc = ts_node_named_child_count(args); char *path = NULL; for (uint32_t i = 0; i < argc; i++) { TSNode arg = ts_node_named_child(args, i); const char *ak = ts_node_type(arg); if (strcmp(ak, "string") == 0 || strcmp(ak, "string_literal") == 0 || strcmp(ak, "template_string") == 0) { path = strip_quotes(a, cbm_node_text(a, arg, ctx->source)); break; } } if (!path || !path[0]) { return false; } /* Infer local name from enclosing variable_declarator. Tree-sitter's JS * grammar wraps `const foo = require(..)` as * lexical_declaration → variable_declarator { name: identifier, value: call } */ const char *local_name = NULL; TSNode parent = ts_node_parent(call); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) { TSNode name_node = ts_node_child_by_field_name(parent, TS_FIELD("name")); if (!ts_node_is_null(name_node) && strcmp(ts_node_type(name_node), "identifier") == 0) { local_name = cbm_node_text(a, name_node, ctx->source); } } if (!local_name) { local_name = path_last(a, path); } CBMImport imp = {.local_name = local_name, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); return true; } static void walk_es_imports(CBMExtractCtx *ctx, TSNode root) { TSNodeStack stack; ts_nstack_init(&stack, ctx->arena, CBM_SZ_512); ts_nstack_push(&stack, ctx->arena, root); while (stack.count > 0) { TSNode node = ts_nstack_pop(&stack); const char *kind = ts_node_type(node); bool push_children = true; if (strcmp(kind, "import_statement") == 0) { if (process_es_import_statement(ctx, node)) { push_children = false; } } else if (strcmp(kind, "export_statement") == 0) { /* Re-export: `export { x } from './mod'` / `export * from './mod'`. * It carries a `source` string just like an import and creates the * same module dependency. */ TSNode src = ts_node_child_by_field_name(node, TS_FIELD("source")); if (!ts_node_is_null(src)) { char *path = strip_quotes(ctx->arena, cbm_node_text(ctx->arena, src, ctx->source)); if (path && path[0]) { CBMImport imp = {.local_name = path_last(ctx->arena, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, ctx->arena, imp); } } } else if (strcmp(kind, "call_expression") == 0) { /* CommonJS require() — only consume the node if we recognized * it as a require call; otherwise keep traversing the children. */ if (process_commonjs_require(ctx, node)) { push_children = false; } } if (push_children) { ts_nstack_push_children(&stack, ctx->arena, node); } } } static void parse_es_imports(CBMExtractCtx *ctx) { walk_es_imports(ctx, ctx->root); } // --- Java imports --- // import_declaration -> scoped_identifier static void parse_java_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); if (strcmp(ts_node_type(node), "import_declaration") != 0) { continue; } // Get the full import path (skip "import" and "static" keywords) uint32_t nc = ts_node_child_count(node); for (uint32_t j = 0; j < nc; j++) { TSNode child = ts_node_child(node, j); const char *ck = ts_node_type(child); if (strcmp(ck, "scoped_identifier") == 0 || strcmp(ck, "identifier") == 0) { char *path = cbm_node_text(a, child, ctx->source); if (path && path[0]) { CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } break; } } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Rust imports --- // use_declaration -> use_list or scoped_use_list static void parse_rust_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); if (strcmp(ts_node_type(node), "use_declaration") != 0) { continue; } char *full = cbm_node_text(a, node, ctx->source); if (!full) { continue; } // Strip "use " prefix and trailing ";" if (strncmp(full, "use ", USE_PREFIX_LEN) == 0) { full += USE_PREFIX_LEN; } size_t len = strlen(full); if (len > 0 && full[len - SKIP_ONE] == ';') { full[len - SKIP_ONE] = '\0'; } CBMImport imp = {.local_name = path_last(a, full), .module_path = full}; cbm_imports_push(&ctx->result->imports, a, imp); } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- C/C++ imports --- // preproc_include -> path or string_literal // Find the path node inside a preproc_include/preproc_import node. static TSNode find_include_path_node(TSNode node) { TSNode path_node = ts_node_child_by_field_name(node, TS_FIELD("path")); if (ts_node_is_null(path_node)) { uint32_t nc = ts_node_child_count(node); for (uint32_t j = 0; j < nc; j++) { TSNode c = ts_node_child(node, j); const char *ck = ts_node_type(c); if (strcmp(ck, "string_literal") == 0 || strcmp(ck, "system_lib_string") == 0) { return c; } } } return path_node; } // Strip angle brackets from a system include path ( → stdio.h). static char *strip_angle_brackets(CBMArena *a, char *path) { if (path && path[0] == '<') { size_t len = strlen(path); if (len > SKIP_ONE && path[len - SKIP_ONE] == '>') { return cbm_arena_strndup(a, path + SKIP_ONE, len - PAIR_LEN); } } return path; } static void parse_c_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); if (strcmp(kind, "preproc_include") != 0 && strcmp(kind, "preproc_import") != 0) { continue; } TSNode path_node = find_include_path_node(node); if (ts_node_is_null(path_node)) { continue; } char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); path = strip_angle_brackets(a, path); if (!path || !path[0]) { continue; } CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Ruby imports --- // call nodes: require("X"), require_relative("X") // Check if a Ruby call node is a require/require_relative, return method name or NULL. static const char *ruby_require_method(CBMArena *a, TSNode node, const char *source) { TSNode method = ts_node_child_by_field_name(node, TS_FIELD("method")); if (ts_node_is_null(method) && ts_node_child_count(node) > 0) { method = ts_node_child(node, 0); } if (ts_node_is_null(method)) { return NULL; } char *name = cbm_node_text(a, method, source); if (!name || (strcmp(name, "require") != 0 && strcmp(name, "require_relative") != 0)) { return NULL; } return name; } // Extract string argument from a Ruby require/require_relative call. static char *extract_ruby_require_arg(CBMArena *a, TSNode node, const char *source) { TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); if (ts_node_is_null(args)) { if (ts_node_child_count(node) > SECOND_IDX) { args = ts_node_child(node, SECOND_IDX); } } if (ts_node_is_null(args)) { return NULL; } uint32_t ac = ts_node_child_count(args); for (uint32_t j = 0; j < ac; j++) { TSNode c = ts_node_child(args, j); const char *ck = ts_node_type(c); if (strcmp(ck, "string") == 0 || strcmp(ck, "string_literal") == 0) { return strip_quotes(a, cbm_node_text(a, c, source)); } } return strip_quotes(a, cbm_node_text(a, args, source)); } static void parse_ruby_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); if (strcmp(kind, "call") != 0 && strcmp(kind, "command_call") != 0) { continue; } if (!ruby_require_method(a, node, ctx->source)) { continue; } char *arg_text = extract_ruby_require_arg(a, node, ctx->source); if (!arg_text || !arg_text[0]) { continue; } CBMImport imp = {.local_name = path_last(a, arg_text), .module_path = arg_text}; cbm_imports_push(&ctx->result->imports, a, imp); } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Lua imports --- // function_call: require("X") static void parse_lua_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); // Lua: local X = require("Y") → assignment_statement or variable_declaration // containing function_call(require, "Y") char *text = cbm_node_text(a, node, ctx->source); if (!text) { continue; } if (strstr(text, "require") == NULL) { continue; } // Simple extraction: find require("...") pattern in node text const char *req = strstr(text, "require"); if (!req) { continue; } // Find the string argument const char *open = strchr(req, '('); if (!open) { open = strchr(req, '"'); } if (!open) { open = strchr(req, '\''); } if (!open) { continue; } const char *q1 = strchr(open, '"'); const char *q2 = strchr(open, '\''); if (!q1 && !q2) { continue; } const char *start = q1 && (!q2 || q1 < q2) ? q1 : q2; char delim = *start; start++; const char *end = strchr(start, delim); if (!end) { continue; } char *mod = cbm_arena_strndup(a, start, (size_t)(end - start)); CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod}; cbm_imports_push(&ctx->result->imports, a, imp); } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- R imports: library()/require()/source() + box::use() (#218) --- // Emit an IMPORTS edge for a module path string (strips a trailing [symbols] // list and surrounding quotes; uses the last path segment as the local name). static void r_push_import(CBMExtractCtx *ctx, const char *raw) { CBMArena *a = ctx->arena; if (!raw || !raw[0]) { return; } char *mod = strip_quotes(a, raw); // box::use specs look like "shiny[moduleServer, NS]" or // "app/logic/validation[validate_input]" — the module path is the part // before the '[' symbol list. char *bracket = strchr(mod, '['); if (bracket) { *bracket = '\0'; } // Trim trailing whitespace left by truncation. size_t len = strlen(mod); while (len > 0 && (mod[len - SKIP_ONE] == ' ' || mod[len - SKIP_ONE] == '\t' || mod[len - SKIP_ONE] == '\n')) { mod[--len] = '\0'; } if (mod[0] == '\0') { return; } CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod}; cbm_imports_push(&ctx->result->imports, a, imp); } // Recursively scan R for import-producing calls. static void r_collect_imports(CBMExtractCtx *ctx, TSNode node) { // NOLINT(misc-no-recursion) if (strcmp(ts_node_type(node), "call") == 0) { TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function")); TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); if (!ts_node_is_null(fn) && !ts_node_is_null(args)) { const char *ft = ts_node_type(fn); if (strcmp(ft, "namespace_operator") == 0) { // box::use(mod[syms], pkg/path[syms], ...) — one IMPORTS per arg. TSNode lhs = ts_node_child_by_field_name(fn, TS_FIELD("lhs")); TSNode rhs = ts_node_child_by_field_name(fn, TS_FIELD("rhs")); char *lt = ts_node_is_null(lhs) ? NULL : cbm_node_text(ctx->arena, lhs, ctx->source); char *rt = ts_node_is_null(rhs) ? NULL : cbm_node_text(ctx->arena, rhs, ctx->source); if (lt && rt && strcmp(lt, "box") == 0 && strcmp(rt, "use") == 0) { uint32_t na = ts_node_named_child_count(args); for (uint32_t i = 0; i < na; i++) { TSNode arg = ts_node_named_child(args, i); if (strcmp(ts_node_type(arg), "argument") != 0) { continue; } TSNode val = ts_node_child_by_field_name(arg, TS_FIELD("value")); if (!ts_node_is_null(val)) { r_push_import(ctx, cbm_node_text(ctx->arena, val, ctx->source)); } } } } else if (strcmp(ft, "identifier") == 0) { // library(pkg) / require(pkg) / requireNamespace("pkg") / // loadNamespace("pkg") / source("file.R") — first arg is the module. char *fname = cbm_node_text(ctx->arena, fn, ctx->source); if (fname && (strcmp(fname, "library") == 0 || strcmp(fname, "require") == 0 || strcmp(fname, "requireNamespace") == 0 || strcmp(fname, "loadNamespace") == 0 || strcmp(fname, "source") == 0)) { uint32_t na = ts_node_named_child_count(args); for (uint32_t i = 0; i < na; i++) { TSNode arg = ts_node_named_child(args, i); if (strcmp(ts_node_type(arg), "argument") != 0) { continue; } TSNode val = ts_node_child_by_field_name(arg, TS_FIELD("value")); if (!ts_node_is_null(val)) { r_push_import(ctx, cbm_node_text(ctx->arena, val, ctx->source)); } break; // first positional argument only } } } } } uint32_t n = ts_node_named_child_count(node); for (uint32_t i = 0; i < n; i++) { r_collect_imports(ctx, ts_node_named_child(node, i)); } } static void parse_r_imports(CBMExtractCtx *ctx) { r_collect_imports(ctx, ctx->root); } // --- Generic import parsing for languages with simple import_declaration --- // Try known field names (path/source/module/name) to extract import path. static bool try_generic_path_fields(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; static const char *path_fields[] = {"path", "source", "module", "name", NULL}; for (const char **f = path_fields; *f; f++) { TSNode path_node = ts_node_child_by_field_name(node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(path_node)) { char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); if (path && path[0]) { CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } return true; } } return false; } // Fallback: extract import path from full node text, stripping keyword and semicolon. static void generic_import_from_text(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; char *text = cbm_node_text(a, node, ctx->source); if (!text || !text[0]) { return; } char *space = strchr(text, ' '); if (space) { text = space + SKIP_ONE; } size_t len = strlen(text); if (len > 0 && text[len - SKIP_ONE] == ';') { text[len - SKIP_ONE] = '\0'; } /* Strip surrounding quotes (Pony `use "util"`, func `#include "utils.fc"`) * so the module path is a clean filename the resolver can match. */ text = strip_quotes(a, text); if (text && text[0]) { CBMImport imp = {.local_name = path_last(a, text), .module_path = text}; cbm_imports_push(&ctx->result->imports, a, imp); } } static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type) { /* Use TSTreeCursor for O(1)-per-step sibling traversal. */ TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); if (strcmp(ts_node_type(node), node_type) != 0) { continue; } if (!try_generic_path_fields(ctx, node)) { generic_import_from_text(ctx, node); } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Kotlin imports --- // tree-sitter-kotlin nests imports: source_file -> import_list -> import_header*. // parse_generic_imports only scans the DIRECT children of root, and "import" is // the keyword token (anon_sym_import), not a statement node — so a generic // match on "import" finds nothing. Descend into import_list (and accept a bare // import_header for grammar variants) and reuse the generic path extractors. static void extract_one_import_header(CBMExtractCtx *ctx, TSNode header) { if (!try_generic_path_fields(ctx, header)) { generic_import_from_text(ctx, header); } } static void parse_kotlin_imports(CBMExtractCtx *ctx) { TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); if (strcmp(kind, "import_header") == 0) { extract_one_import_header(ctx, node); } else if (strcmp(kind, "import_list") == 0) { uint32_t nc = ts_node_child_count(node); for (uint32_t j = 0; j < nc; j++) { TSNode child = ts_node_child(node, j); if (strcmp(ts_node_type(child), "import_header") == 0) { extract_one_import_header(ctx, child); } } } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // Find the first descendant node of `type` (DFS, pre-order). Returns true and // writes *out on the first match. Shared by the Dart/Zig import parsers, whose // URI/string is nested several levels below the import node. static bool find_first_descendant_of(TSNode node, const char *type, // NOLINT(misc-no-recursion) TSNode *out) { if (strcmp(ts_node_type(node), type) == 0) { *out = node; return true; } uint32_t n = ts_node_named_child_count(node); for (uint32_t i = 0; i < n; i++) { if (find_first_descendant_of(ts_node_named_child(node, i), type, out)) { return true; } } return false; } // --- Dart imports --- // tree-sitter-dart wraps each top-level import as `import_or_export`; the URI is // a `string_literal` nested under library_import -> import_specification -> // configurable_uri -> uri. The old dispatch matched "import_declaration" (which // tree-sitter-dart never emits) -> 0 imports. Find the first string_literal under // each import_or_export and strip its quotes. static void parse_dart_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); if (strcmp(ts_node_type(node), "import_or_export") != 0) { continue; } TSNode uri = node; if (find_first_descendant_of(node, "string_literal", &uri)) { char *path = strip_quotes(a, cbm_node_text(a, uri, ctx->source)); if (path && path[0]) { CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Haskell imports --- // tree-sitter-haskell nests imports under an `imports` container (and/or lists // `import` nodes); each `import` carries a `module` field. parse_generic_imports // only scanned root children for "import", missing the container -> 0 imports. // Descend into `imports` (and accept a root-level `import`) and reuse the generic // path extractors, which pick up the "module" field. static void parse_haskell_imports(CBMExtractCtx *ctx) { TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); if (strcmp(kind, "import") == 0) { extract_one_import_header(ctx, node); } else if (strcmp(kind, "imports") == 0) { uint32_t nc = ts_node_child_count(node); for (uint32_t j = 0; j < nc; j++) { TSNode child = ts_node_child(node, j); if (strcmp(ts_node_type(child), "import") == 0) { extract_one_import_header(ctx, child); } } } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Zig imports --- // Zig imports are `const std = @import("std");` — a `builtin_function` (@import) // nested inside a variable_declaration, NOT a root child. The old dispatch scanned // root for "builtin_function" -> 0. DFS the whole tree for builtin_function nodes // whose text starts with @import/@cImport and take their first string argument. static void parse_zig_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSNodeStack stack; ts_nstack_init(&stack, a, CBM_SZ_512); ts_nstack_push(&stack, a, ctx->root); while (stack.count > 0) { TSNode node = ts_nstack_pop(&stack); if (strcmp(ts_node_type(node), "builtin_function") == 0) { char *bf = cbm_node_text(a, node, ctx->source); if (bf && (strncmp(bf, "@import", sizeof("@import") - 1) == 0 || strncmp(bf, "@cImport", sizeof("@cImport") - 1) == 0)) { TSNode str = node; if (find_first_descendant_of(node, "string", &str)) { char *path = strip_quotes(a, cbm_node_text(a, str, ctx->source)); if (path && path[0]) { CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } } } } ts_nstack_push_children(&stack, a, node); } } // --- Wolfram imports --- // get_top: << "package" (Get["file"]) // apply where first child is builtin_symbol "Needs" with string arg // Handle Wolfram get_top: << "path" → import. static void process_wolfram_get_top(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode child = ts_node_named_child(node, i); const char *ck = ts_node_type(child); if (strcmp(ck, "string") == 0 || strcmp(ck, "user_symbol") == 0) { char *text = cbm_node_text(a, child, ctx->source); if (text && text[0]) { char *path = strip_quotes(a, text); CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } break; } } } // Handle Wolfram Needs["package`"] — apply where head is builtin_symbol "Needs". static void process_wolfram_needs(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; if (ts_node_named_child_count(node) < MIN_WOLFRAM_CHILDREN) { return; } TSNode head = ts_node_named_child(node, 0); if (strcmp(ts_node_type(head), "builtin_symbol") != 0) { return; } char *name = cbm_node_text(a, head, ctx->source); if (!name || strcmp(name, "Needs") != 0) { return; } TSNode arg = ts_node_named_child(node, SECOND_IDX); char *text = cbm_node_text(a, arg, ctx->source); if (text && text[0]) { char *path = strip_quotes(a, text); CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } } static void walk_wolfram_imports(CBMExtractCtx *ctx, TSNode root) { TSNodeStack stack; ts_nstack_init(&stack, ctx->arena, CBM_SZ_512); ts_nstack_push(&stack, ctx->arena, root); while (stack.count > 0) { TSNode node = ts_nstack_pop(&stack); const char *kind = ts_node_type(node); if (strcmp(kind, "get_top") == 0) { process_wolfram_get_top(ctx, node); } else if (strcmp(kind, "apply") == 0) { process_wolfram_needs(ctx, node); } ts_nstack_push_children(&stack, ctx->arena, node); } } static void parse_wolfram_imports(CBMExtractCtx *ctx) { walk_wolfram_imports(ctx, ctx->root); } // --- PHP imports --- // tree-sitter-php models `use Foo\Bar;` as a `namespace_use_declaration` // containing one or more `namespace_use_clause` nodes (each a qualified_name, // optionally aliased via `as`). Grouped `use Foo\{A, B};` uses a // `namespace_use_group` with `namespace_use_group_clause` children. The bare // require/include forms remain `expression_statement`s and are still handled by // the text fallback. Take the first qualified_name/name descendant of each // clause as the module path. static void emit_php_use_clause(CBMExtractCtx *ctx, TSNode clause, const char *group_prefix) { CBMArena *a = ctx->arena; // The path node is the qualified_name / namespace_name / name child. TSNode path_node = clause; bool found = false; static const char *path_kinds[] = {"qualified_name", "namespace_name", "name", NULL}; for (const char **k = path_kinds; *k && !found; k++) { if (find_first_descendant_of(clause, *k, &path_node)) { found = true; } } if (!found) { return; } char *path = cbm_node_text(a, path_node, ctx->source); if (!path || !path[0]) { return; } if (group_prefix && group_prefix[0]) { path = cbm_arena_sprintf(a, "%s\\%s", group_prefix, path); } // Alias: an `as` clause provides a trailing identifier (the second name). TSNode alias = ts_node_child_by_field_name(clause, TS_FIELD("alias")); const char *local = !ts_node_is_null(alias) ? cbm_node_text(a, alias, ctx->source) : path_last(a, path); CBMImport imp = {.local_name = local, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } static void emit_php_use_decl(CBMExtractCtx *ctx, TSNode decl) { CBMArena *a = ctx->arena; // Grouped form: namespace_use_group with a leading prefix qualified_name. TSNode group = decl; if (find_first_descendant_of(decl, "namespace_use_group", &group)) { // The prefix is the qualified_name sibling preceding the group within decl. char *prefix = NULL; uint32_t dc = ts_node_named_child_count(decl); for (uint32_t i = 0; i < dc; i++) { TSNode c = ts_node_named_child(decl, i); const char *ck = ts_node_type(c); if (strcmp(ck, "qualified_name") == 0 || strcmp(ck, "namespace_name") == 0 || strcmp(ck, "name") == 0) { prefix = cbm_node_text(a, c, ctx->source); break; } } uint32_t gc = ts_node_named_child_count(group); for (uint32_t i = 0; i < gc; i++) { TSNode clause = ts_node_named_child(group, i); const char *ck = ts_node_type(clause); if (strcmp(ck, "namespace_use_group_clause") == 0 || strcmp(ck, "namespace_use_clause") == 0) { emit_php_use_clause(ctx, clause, prefix); } } return; } // Flat form: one or more namespace_use_clause children. uint32_t dc = ts_node_named_child_count(decl); bool any = false; for (uint32_t i = 0; i < dc; i++) { TSNode clause = ts_node_named_child(decl, i); if (strcmp(ts_node_type(clause), "namespace_use_clause") == 0) { emit_php_use_clause(ctx, clause, NULL); any = true; } } if (!any) { // Some grammar versions inline the path directly under the declaration. emit_php_use_clause(ctx, decl, NULL); } } static void parse_php_imports(CBMExtractCtx *ctx) { TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); if (strcmp(kind, "namespace_use_declaration") == 0) { emit_php_use_decl(ctx, node); } else if (strcmp(kind, "expression_statement") == 0) { // require / include / require_once / include_once if (!try_generic_path_fields(ctx, node)) { generic_import_from_text(ctx, node); } } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- C# imports --- // tree-sitter-c-sharp models `using System.Text;` as a `using_directive`. // The namespace path is a `qualified_name`/`identifier` child. For an alias // form `using F = System.IO.File;` the directive has a `name` field holding the // alias identifier `F` and a separate qualified_name on the right of `=` — the // generic path-field extractor wrongly returns the alias `F`, so we instead // take the LAST qualified_name/identifier (the real namespace/type), and use // the `name` field as local_name when present. static void parse_csharp_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); if (strcmp(ts_node_type(node), "using_directive") != 0) { continue; } // Find the right-most qualified_name / identifier (the namespace/type), // which is the import target even in the alias form `using F = X;`. TSNode path_node = node; bool found = false; uint32_t nc = ts_node_named_child_count(node); for (int i = (int)nc - 1; i >= 0; i--) { TSNode c = ts_node_named_child(node, (uint32_t)i); const char *ck = ts_node_type(c); if (strcmp(ck, "qualified_name") == 0 || strcmp(ck, "identifier") == 0 || strcmp(ck, "member_access_expression") == 0 || strcmp(ck, "name") == 0) { path_node = c; found = true; break; } } char *path = found ? cbm_node_text(a, path_node, ctx->source) : NULL; if (!path || !path[0]) { // Fallback to text stripping (handles `using static X;`). if (!try_generic_path_fields(ctx, node)) { generic_import_from_text(ctx, node); } continue; } // Alias name, if any. TSNode alias = ts_node_child_by_field_name(node, TS_FIELD("alias")); const char *local = !ts_node_is_null(alias) ? cbm_node_text(a, alias, ctx->source) : path_last(a, path); CBMImport imp = {.local_name = local, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Generic spec-driven imports --- // For grammar-only languages that have no dedicated parser above, consume the // `import_node_types` declared in the language's CBMLangSpec. Each root-level // child whose type matches one of those node types is treated as an import: // first try the known path fields (path/source/module/name), then fall back to // stripping the leading keyword + trailing ';' from the node text. This is the // same extraction strategy the dedicated `parse_generic_imports` used, but the // node-type set comes from the spec instead of a hardcoded string, so every // language with `import_node_types` configured gets imports extracted. static bool spec_type_matches(const char **types, const char *kind) { if (!types) { return false; } for (const char **t = types; *t; t++) { if (strcmp(*t, kind) == 0) { return true; } } return false; } static void parse_spec_imports(CBMExtractCtx *ctx) { const CBMLangSpec *spec = cbm_lang_spec(ctx->language); if (!spec || !spec->import_node_types) { return; } TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode node = ts_tree_cursor_current_node(&cursor); const char *kind = ts_node_type(node); if (!spec_type_matches(spec->import_node_types, kind)) { continue; } if (!try_generic_path_fields(ctx, node)) { generic_import_from_text(ctx, node); } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } // --- Embedded-language imports --- // Generic walker for host grammars (Svelte, Vue, HTML, Astro, ...) whose AST // keeps embedded sub-language source as raw_text (or similar) without parsing // it. The host's CBMLangSpec.embedded_imports declares which content nodes // hold which sub-language; we re-parse each match with the embedded grammar // and run the standard ES import walker over the inner AST. // // No grammar symbols are referenced here — the embedded TSLanguage is // resolved through cbm_ts_language(spec->embedded_language), the same hook // that the main parser uses. Adding another host language is a one-line // declaration in lang_specs.c. static void embedded_collect_content_nodes(TSNode root, const CBMEmbeddedLangSpec *spec, TSNode *out, int *out_count, int max_out) { /* Iterative DFS so deeply-nested script blocks are still found. Cap the * stack to a sane bound (host grammars do not have million-deep markup * trees) — no need to introduce TSNodeStack here. */ enum { EMBED_STACK_CAP = 1024 }; TSNode stack[EMBED_STACK_CAP]; int top = 0; stack[top++] = root; while (top > 0 && *out_count < max_out) { TSNode node = stack[--top]; const char *kind = ts_node_type(node); if (strcmp(kind, spec->script_node_type) == 0) { uint32_t cc = ts_node_child_count(node); for (uint32_t k = 0; k < cc; k++) { TSNode c = ts_node_child(node, k); if (strcmp(ts_node_type(c), spec->content_node_type) == 0) { out[(*out_count)++] = c; if (*out_count >= max_out) { return; } break; /* one content node per script element */ } } /* Do not descend into