mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(imports): extract ES imports from embedded <script> blocks (#224)
Generalizes #224's Svelte/Vue script-block import extraction into a spec-driven embedded-language pattern that also covers HTML and Astro, so any future host grammar that keeps sub-language source as raw text gets coverage with a one-line declaration. The existing CBMLangSpec already exposes the per-language tree-sitter grammar via cbm_ts_language(); add a parallel embedded_imports field that lists which AST node types hold which sub-language. The new generic walker in extract_imports.c reads that list, locates each embedded content node in the host AST, re-parses the slice with the embedded grammar resolved through cbm_ts_language(), and runs the standard ES import extractor (walk_es_imports) over the inner AST. No grammar symbols are referenced from the extraction file. - internal/cbm/lang_specs.h: add CBMEmbeddedLangSpec + embedded_imports field on CBMLangSpec. - internal/cbm/lang_specs.c: declare vue/svelte/html/astro embed lists ({"script_element", "raw_text", CBM_LANG_JAVASCRIPT}); set the new field on those four spec entries; explicit NULL initializer on every other entry to satisfy -Wmissing-field-initializers. - internal/cbm/extract_imports.c: add embedded_collect_content_nodes + parse_embedded_imports; route SVELTE/VUE/HTML/ASTRO through the generic helper. - tests/test_extraction.c: cover Svelte (with and without <script>), Vue, and HTML. Full suite: 2824 passed, 0 failed. Closes #223. Closes #224. Co-authored-by: James <jmcmacnz@gmail.com>
This commit is contained in:
1 parent
aa6df6b906
commit
30941983cb
4 files changed
+385
-155
No files matched your search
@@ -1,6 +1,7 @@
|
||||
#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 "ts_node_stack.h"
|
||||
@@ -846,6 +847,99 @@ static void parse_wolfram_imports(CBMExtractCtx *ctx) {
|
||||
walk_wolfram_imports(ctx, ctx->root);
|
||||
}
|
||||
|
||||
// --- 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 <script>'s children — content already taken. */
|
||||
continue;
|
||||
}
|
||||
uint32_t count = ts_node_child_count(node);
|
||||
for (int i = (int)count - 1; i >= 0 && top < EMBED_STACK_CAP; i--) {
|
||||
stack[top++] = ts_node_child(node, (uint32_t)i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void parse_embedded_imports(CBMExtractCtx *ctx) {
|
||||
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
|
||||
if (!spec || !spec->embedded_imports) {
|
||||
return;
|
||||
}
|
||||
for (const CBMEmbeddedLangSpec *e = spec->embedded_imports; e->script_node_type != NULL; e++) {
|
||||
const TSLanguage *embedded_lang = cbm_ts_language(e->embedded_language);
|
||||
if (!embedded_lang) {
|
||||
continue; /* embedded grammar not linked in — silently skip */
|
||||
}
|
||||
enum { MAX_EMBEDDED_BLOCKS = 16 };
|
||||
TSNode hits[MAX_EMBEDDED_BLOCKS];
|
||||
int hit_count = 0;
|
||||
embedded_collect_content_nodes(ctx->root, e, hits, &hit_count, MAX_EMBEDDED_BLOCKS);
|
||||
if (hit_count == 0) {
|
||||
continue;
|
||||
}
|
||||
TSParser *parser = ts_parser_new();
|
||||
if (!parser) {
|
||||
continue;
|
||||
}
|
||||
if (!ts_parser_set_language(parser, embedded_lang)) {
|
||||
ts_parser_delete(parser);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < hit_count; i++) {
|
||||
uint32_t s = ts_node_start_byte(hits[i]);
|
||||
uint32_t end = ts_node_end_byte(hits[i]);
|
||||
if (end <= s) {
|
||||
continue;
|
||||
}
|
||||
const char *sub_src = ctx->source + s;
|
||||
uint32_t sub_len = end - s;
|
||||
TSTree *sub_tree = ts_parser_parse_string(parser, NULL, sub_src, sub_len);
|
||||
if (!sub_tree) {
|
||||
continue;
|
||||
}
|
||||
CBMExtractCtx sub_ctx = *ctx;
|
||||
sub_ctx.source = sub_src;
|
||||
sub_ctx.root = ts_tree_root_node(sub_tree);
|
||||
walk_es_imports(&sub_ctx, sub_ctx.root);
|
||||
ts_tree_delete(sub_tree);
|
||||
}
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main dispatch ---
|
||||
|
||||
void cbm_extract_imports(CBMExtractCtx *ctx) {
|
||||
@@ -937,6 +1031,14 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
|
||||
case CBM_LANG_WOLFRAM:
|
||||
parse_wolfram_imports(ctx);
|
||||
break;
|
||||
/* Host languages whose tree-sitter grammar leaves <script> bodies as raw
|
||||
* text — re-parse the embedded slice via the embedded-language spec. */
|
||||
case CBM_LANG_SVELTE:
|
||||
case CBM_LANG_VUE:
|
||||
case CBM_LANG_HTML:
|
||||
case CBM_LANG_ASTRO:
|
||||
parse_embedded_imports(ctx);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+181
-155
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,19 @@
|
||||
|
||||
#include "cbm.h"
|
||||
|
||||
// CBMEmbeddedLangSpec describes a sub-language embedded inside a host AST.
|
||||
// Used by host grammars whose tree-sitter parser does not recurse into the
|
||||
// embedded content (e.g. Svelte, Vue, Astro: script_element -> raw_text holds
|
||||
// JavaScript verbatim that the host grammar leaves unparsed). The generic
|
||||
// embedded-imports walker locates each script_node_type in the host AST,
|
||||
// finds its content_node_type child, and re-parses that slice with the
|
||||
// embedded_language's grammar so existing extractors run on the inner AST.
|
||||
typedef struct {
|
||||
const char *script_node_type; // e.g. "script_element"
|
||||
const char *content_node_type; // e.g. "raw_text"
|
||||
CBMLanguage embedded_language; // grammar used to re-parse the content slice
|
||||
} CBMEmbeddedLangSpec;
|
||||
|
||||
// CBMLangSpec mirrors Go's lang.LanguageSpec with NULL-terminated string arrays.
|
||||
typedef struct {
|
||||
CBMLanguage language;
|
||||
@@ -22,6 +35,10 @@ typedef struct {
|
||||
const char **env_access_functions; // NULL-terminated (NULL if none)
|
||||
const char **env_access_member_patterns; // NULL-terminated (NULL if none)
|
||||
const TSLanguage *(*ts_factory)(void); // Tree-sitter grammar factory (NULL if shared)
|
||||
// NULL-terminated list of embedded sub-languages (NULL if host grammar has
|
||||
// no embedded content to re-parse). The terminator is an entry whose
|
||||
// script_node_type is NULL.
|
||||
const CBMEmbeddedLangSpec *embedded_imports;
|
||||
} CBMLangSpec;
|
||||
|
||||
// Get the language spec for a given language. Returns NULL for unsupported.
|
||||
|
||||
@@ -1769,6 +1769,87 @@ TEST(import_stress_go) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* Embedded-language import extraction
|
||||
* Host grammars (Svelte, Vue, HTML, Astro) keep <script> bodies as
|
||||
* raw_text — the embedded-imports walker re-parses each block with the
|
||||
* JS grammar so the standard ES import extractor sees real
|
||||
* import_statement nodes.
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(svelte_imports_basic) {
|
||||
/* Default import + named imports + namespace import */
|
||||
CBMFileResult *r = extract(
|
||||
"<script>\n"
|
||||
"import Foo from './Foo.svelte';\n"
|
||||
"import { bar, baz } from '../lib/utils';\n"
|
||||
"import * as helpers from './helpers';\n"
|
||||
"export let value = 42;\n"
|
||||
"</script>\n"
|
||||
"<h1>Hello {value}</h1>\n",
|
||||
CBM_LANG_SVELTE, "t", "Comp.svelte");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_GTE(r->imports.count, 3);
|
||||
ASSERT(has_import(r, "Foo.svelte"));
|
||||
ASSERT(has_import(r, "lib/utils"));
|
||||
ASSERT(has_import(r, "helpers"));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(svelte_imports_no_script) {
|
||||
/* .svelte with no <script> block must not crash, 0 imports */
|
||||
CBMFileResult *r = extract(
|
||||
"<h1>Static page</h1>\n"
|
||||
"<p>No script here.</p>\n",
|
||||
CBM_LANG_SVELTE, "t", "Static.svelte");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_EQ(r->imports.count, 0);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vue_imports_basic) {
|
||||
/* Vue SFC: same document→script_element→raw_text AST structure */
|
||||
CBMFileResult *r = extract(
|
||||
"<template><div>{{ msg }}</div></template>\n"
|
||||
"<script>\n"
|
||||
"import MyComp from './MyComp.vue';\n"
|
||||
"import { ref } from 'vue';\n"
|
||||
"export default { name: 'App' };\n"
|
||||
"</script>\n",
|
||||
CBM_LANG_VUE, "t", "App.vue");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_GTE(r->imports.count, 2);
|
||||
ASSERT(has_import(r, "MyComp.vue"));
|
||||
ASSERT(has_import(r, "vue"));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(html_imports_basic) {
|
||||
/* Plain HTML with inline ES module imports — same generic walker. */
|
||||
CBMFileResult *r = extract(
|
||||
"<!DOCTYPE html><html><head>\n"
|
||||
"<script type=\"module\">\n"
|
||||
"import { renderApp } from './app.js';\n"
|
||||
"import * as utils from './utils.js';\n"
|
||||
"renderApp();\n"
|
||||
"</script>\n"
|
||||
"</head><body></body></html>\n",
|
||||
CBM_LANG_HTML, "t", "index.html");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_GTE(r->imports.count, 2);
|
||||
ASSERT(has_import(r, "app.js"));
|
||||
ASSERT(has_import(r, "utils.js"));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* config_extraction_test.go ports (25 tests)
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
@@ -2297,6 +2378,10 @@ SUITE(extraction) {
|
||||
RUN_TEST(ruby_imports);
|
||||
RUN_TEST(lua_imports);
|
||||
RUN_TEST(import_stress_go);
|
||||
RUN_TEST(svelte_imports_basic);
|
||||
RUN_TEST(svelte_imports_no_script);
|
||||
RUN_TEST(vue_imports_basic);
|
||||
RUN_TEST(html_imports_basic);
|
||||
|
||||
/* config_extraction_test.go ports */
|
||||
RUN_TEST(toml_basic_table_and_pair);
|
||||
|
||||
Reference in new issue
Block a user