mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-03 08:03:38 +08:00
Merge pull request #2056 from bmcnaboe/fix/svelte-html-astro-embedded-structure
fix(extract): extract structure from Svelte/HTML/Astro inline scripts
This commit is contained in:
@@ -1383,12 +1383,13 @@ static void parse_spec_imports(CBMExtractCtx *ctx) {
|
||||
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.
|
||||
// --- Embedded-language structure and imports ---
|
||||
// Generic walker for host grammars (Svelte, Vue, HTML, Astro, CFML, ...) 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 definition, import and call extractors over the inner
|
||||
// AST, so a component's script contributes symbols like a standalone file.
|
||||
//
|
||||
// No grammar symbols are referenced here — the embedded TSLanguage is
|
||||
// resolved through cbm_ts_language(spec->embedded_language), the same hook
|
||||
@@ -1458,66 +1459,94 @@ static bool ascii_trimmed_equals(const char *value, const char *expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Vue is the only host that opts into structural embedded extraction. Restrict
|
||||
* its inline programs to the language forms whose parsers we can select
|
||||
* exactly. A src attribute always denotes an external program and therefore
|
||||
* suppresses any inline extraction, even for malformed mixed markup. */
|
||||
static bool vue_embedded_language(CBMExtractCtx *ctx, TSNode script, CBMLanguage *language) {
|
||||
enum { VUE_ATTR_STACK_CAP = 128 };
|
||||
TSNode stack[VUE_ATTR_STACK_CAP];
|
||||
/* The text of a <script> attribute's value, or "" when it carries none
|
||||
* (`<script type>`), so every attribute reads through one path. */
|
||||
static const char *script_attribute_value(CBMExtractCtx *ctx, TSNode attribute) {
|
||||
TSNode value_node = attribute;
|
||||
if (!find_first_descendant_of(attribute, "attribute_value", &value_node)) {
|
||||
return "";
|
||||
}
|
||||
return cbm_node_text(ctx->arena, value_node, ctx->source);
|
||||
}
|
||||
|
||||
/* Apply one <script> attribute to the parse decision. Returns false when the
|
||||
* attribute rules the block out of inline extraction:
|
||||
* src= always names an external program, so nothing inline is extracted,
|
||||
* even for malformed mixed markup;
|
||||
* lang= selects the JavaScript or TypeScript grammar; any other language
|
||||
* has no parser here;
|
||||
* type= plain HTML routinely carries non-program blocks (application/json,
|
||||
* importmap, text/x-template, application/ld+json) that would parse
|
||||
* into garbage definitions and calls. Only the JavaScript forms in
|
||||
* current use pass — absent or empty, module, text/javascript,
|
||||
* application/javascript — and the rest bail. */
|
||||
static bool apply_script_attribute(CBMExtractCtx *ctx, TSNode attribute, CBMLanguage *language) {
|
||||
if (ts_node_named_child_count(attribute) == 0) {
|
||||
return false;
|
||||
}
|
||||
const char *name = cbm_node_text(ctx->arena, ts_node_named_child(attribute, 0), ctx->source);
|
||||
if (ascii_trimmed_equals(name, "src")) {
|
||||
return false;
|
||||
}
|
||||
if (ascii_trimmed_equals(name, "lang")) {
|
||||
const char *value = script_attribute_value(ctx, attribute);
|
||||
if (ascii_trimmed_equals(value, "js") || ascii_trimmed_equals(value, "javascript")) {
|
||||
*language = CBM_LANG_JAVASCRIPT;
|
||||
return true;
|
||||
}
|
||||
if (ascii_trimmed_equals(value, "ts") || ascii_trimmed_equals(value, "typescript")) {
|
||||
*language = CBM_LANG_TYPESCRIPT;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (ascii_trimmed_equals(name, "type")) {
|
||||
const char *value = script_attribute_value(ctx, attribute);
|
||||
return ascii_trimmed_equals(value, "") || ascii_trimmed_equals(value, "module") ||
|
||||
ascii_trimmed_equals(value, "text/javascript") ||
|
||||
ascii_trimmed_equals(value, "application/javascript");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Markup hosts (Vue, Svelte, HTML, Astro) hang the attributes that decide
|
||||
* whether and how an inline program is parsed off the <script> start tag.
|
||||
* *language enters holding the spec row's default and leaves holding the
|
||||
* grammar to parse with; an attribute-free block (Astro's frontmatter fence)
|
||||
* keeps the row's language rather than falling through to JavaScript. */
|
||||
static bool script_embedded_language(CBMExtractCtx *ctx, TSNode script, CBMLanguage *language) {
|
||||
enum { SCRIPT_ATTR_STACK_CAP = 128 };
|
||||
TSNode stack[SCRIPT_ATTR_STACK_CAP];
|
||||
int top = 0;
|
||||
bool has_lang = false;
|
||||
bool lang_supported = true;
|
||||
CBMLanguage selected = CBM_LANG_JAVASCRIPT;
|
||||
stack[top++] = script;
|
||||
while (top > 0) {
|
||||
TSNode node = stack[--top];
|
||||
if (strcmp(ts_node_type(node), "attribute") == 0) {
|
||||
uint32_t named_count = ts_node_named_child_count(node);
|
||||
if (named_count == 0) {
|
||||
if (!apply_script_attribute(ctx, node, language)) {
|
||||
return false;
|
||||
}
|
||||
TSNode name_node = ts_node_named_child(node, 0);
|
||||
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
|
||||
if (ascii_trimmed_equals(name, "src")) {
|
||||
return false;
|
||||
}
|
||||
if (ascii_trimmed_equals(name, "lang")) {
|
||||
TSNode value_node = node;
|
||||
if (!find_first_descendant_of(node, "attribute_value", &value_node)) {
|
||||
return false;
|
||||
}
|
||||
char *value = cbm_node_text(ctx->arena, value_node, ctx->source);
|
||||
has_lang = true;
|
||||
if (ascii_trimmed_equals(value, "js") ||
|
||||
ascii_trimmed_equals(value, "javascript")) {
|
||||
selected = CBM_LANG_JAVASCRIPT;
|
||||
} else if (ascii_trimmed_equals(value, "ts") ||
|
||||
ascii_trimmed_equals(value, "typescript")) {
|
||||
selected = CBM_LANG_TYPESCRIPT;
|
||||
} else {
|
||||
lang_supported = false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if ((int)count > VUE_ATTR_STACK_CAP - top) {
|
||||
if ((int)count > SCRIPT_ATTR_STACK_CAP - top) {
|
||||
return false;
|
||||
}
|
||||
for (int i = (int)count - 1; i >= 0; i--) {
|
||||
stack[top++] = ts_node_named_child(node, (uint32_t)i);
|
||||
}
|
||||
}
|
||||
if (has_lang && !lang_supported) {
|
||||
return false;
|
||||
}
|
||||
*language = selected;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* CFML's <cfscript> carries no attributes and its embedded language is fixed
|
||||
* by the spec row; only the markup hosts consult the <script> tag. */
|
||||
static bool markup_script_host(CBMLanguage language) {
|
||||
return language == CBM_LANG_VUE || language == CBM_LANG_SVELTE || language == CBM_LANG_HTML ||
|
||||
language == CBM_LANG_ASTRO;
|
||||
}
|
||||
|
||||
static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock *block,
|
||||
CBMLanguage embedded_language, bool extract_structure) {
|
||||
CBMLanguage embedded_language) {
|
||||
const TSLanguage *language = cbm_ts_language(embedded_language);
|
||||
if (!language || ctx->source_len < 0) {
|
||||
return;
|
||||
@@ -1564,13 +1593,9 @@ static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock
|
||||
.macro_table = ctx->macro_table,
|
||||
.return_type_table = ctx->return_type_table,
|
||||
};
|
||||
if (extract_structure) {
|
||||
cbm_extract_definitions_without_module(&sub_ctx);
|
||||
}
|
||||
cbm_extract_definitions_without_module(&sub_ctx);
|
||||
walk_es_imports(&sub_ctx, sub_ctx.root);
|
||||
if (extract_structure) {
|
||||
cbm_extract_unified(&sub_ctx);
|
||||
}
|
||||
cbm_extract_unified(&sub_ctx);
|
||||
|
||||
ts_tree_delete(tree);
|
||||
ts_parser_delete(parser);
|
||||
@@ -1588,22 +1613,11 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
|
||||
embedded_collect_content_nodes(ctx->root, e, hits, &hit_count, MAX_EMBEDDED_BLOCKS);
|
||||
for (int i = 0; i < hit_count; i++) {
|
||||
CBMLanguage embedded_language = e->embedded_language;
|
||||
/* Structure (defs + calls), not just imports, for hosts whose
|
||||
* embedded language carries real code. Vue since #1852; CFML's
|
||||
* <cfscript> since the #1412 distillation. */
|
||||
bool extract_structure =
|
||||
ctx->language == CBM_LANG_VUE || ctx->language == CBM_LANG_CFML;
|
||||
/* The attribute resolver is Vue's: it inspects <script lang=/src=>
|
||||
* and OVERRIDES the spec's embedded language (JS default, TS on
|
||||
* lang="ts", bail on src=). CFML's cf_script_tag carries no such
|
||||
* attributes and its embedded language is fixed by the spec row
|
||||
* (CFSCRIPT) — running the resolver would silently rewrite it to
|
||||
* JavaScript. Resolve only for Vue. */
|
||||
if (ctx->language == CBM_LANG_VUE &&
|
||||
!vue_embedded_language(ctx, hits[i].script, &embedded_language)) {
|
||||
if (markup_script_host(ctx->language) &&
|
||||
!script_embedded_language(ctx, hits[i].script, &embedded_language)) {
|
||||
continue;
|
||||
}
|
||||
parse_one_embedded_block(ctx, &hits[i], embedded_language, extract_structure);
|
||||
parse_one_embedded_block(ctx, &hits[i], embedded_language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,8 +890,9 @@ static const char *graphql_field_types[] = {"field_definition", "input_value_def
|
||||
// ==================== Embedded sub-languages ====================
|
||||
// Host grammars (Svelte/Vue/HTML/Astro) treat <script> bodies as raw_text and
|
||||
// do not recurse into them. Declaring the host's script-content node here lets
|
||||
// the generic embedded-imports walker re-parse that slice with the JS grammar
|
||||
// so the existing ES import extractor sees real import_statement nodes.
|
||||
// the generic embedded walker re-parse that slice with the declared grammar so
|
||||
// its definitions, imports and calls extract in host-file coordinates. The
|
||||
// declared language is the default; a <script lang=> attribute overrides it.
|
||||
// Terminator: an entry whose script_node_type is NULL.
|
||||
static const CBMEmbeddedLangSpec cfml_embedded_imports[] = {
|
||||
/* Tag-dialect CFML keeps <cfscript> bodies as opaque cf_script_content;
|
||||
@@ -915,10 +916,12 @@ static const CBMEmbeddedLangSpec html_embedded_imports[] = {
|
||||
};
|
||||
static const CBMEmbeddedLangSpec astro_embedded_imports[] = {
|
||||
/* Astro component scripts live in the `---` frontmatter fence, which the
|
||||
* grammar keeps as an unparsed frontmatter_js_block. Re-parse that slice
|
||||
* with the JS grammar so `import X from './X.astro'` becomes a real edge. */
|
||||
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT},
|
||||
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
|
||||
* grammar keeps as an unparsed frontmatter_js_block. Astro type-checks the
|
||||
* fence and its <script> bodies as TypeScript without any lang= attribute
|
||||
* saying so (the fence cannot carry one), so TypeScript is the deliberate
|
||||
* default for both; it parses untyped JavaScript unchanged. */
|
||||
{"frontmatter", "frontmatter_js_block", CBM_LANG_TYPESCRIPT},
|
||||
{"script_element", "raw_text", CBM_LANG_TYPESCRIPT},
|
||||
{NULL, NULL, 0},
|
||||
};
|
||||
|
||||
|
||||
@@ -420,6 +420,19 @@ TEST(es_calls_vue_embedded_issue1410) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(es_calls_svelte_embedded_issue1807) {
|
||||
static const ES_LangFile f[] = {
|
||||
{"Toggle.svelte",
|
||||
"<script lang=\"ts\">\n"
|
||||
"function callee(): number { return 42; }\n"
|
||||
"function caller(): number { return callee(); }\n"
|
||||
"</script>\n"
|
||||
"<button on:click={caller}>Toggle</button>\n"},
|
||||
};
|
||||
ASSERT_EQ(es_exact_edge_by_name(f, 1, "CALLS", "caller", "callee"), 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Java: caller in Main.java calls static method from Util.java (same package). */
|
||||
TEST(es_calls_crossfile_java) {
|
||||
static const ES_LangFile f[] = {
|
||||
@@ -927,6 +940,7 @@ SUITE(edge_structural) {
|
||||
RUN_TEST(es_calls_crossfile_kotlin);
|
||||
RUN_TEST(es_calls_crossfile_csharp);
|
||||
RUN_TEST(es_calls_vue_embedded_issue1410);
|
||||
RUN_TEST(es_calls_svelte_embedded_issue1807);
|
||||
|
||||
/* ── FAMILY 2: INHERITS cross-file ────────────────────────── */
|
||||
/* GREEN: Java, C#, C++ (extraction confirmed correct). */
|
||||
|
||||
+184
-10
@@ -3372,7 +3372,10 @@ TEST(vue_embedded_structure_negative_controls_issue1410) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vue_embedded_structure_host_controls_issue1410) {
|
||||
/* The sibling hosts ride Vue's embedded seam: the function and call a .ts file
|
||||
* yields come out of a plain <script> (or Astro's frontmatter fence) the same
|
||||
* way, alongside the import those blocks always produced. */
|
||||
TEST(embedded_structure_sibling_hosts_issue1807) {
|
||||
CBMFileResult *plain =
|
||||
extract("function plainTs(): void { target(); }\n", CBM_LANG_TYPESCRIPT, "t", "plain.ts");
|
||||
ASSERT_NOT_NULL(plain);
|
||||
@@ -3386,26 +3389,193 @@ TEST(vue_embedded_structure_host_controls_issue1410) {
|
||||
const char *path;
|
||||
const char *source;
|
||||
} hosts[] = {
|
||||
{CBM_LANG_SVELTE, "Control.svelte",
|
||||
"<script>import value from './svelte.js'; function hidden() { target(); }</script>\n"},
|
||||
{CBM_LANG_HTML, "control.html",
|
||||
"<script>import value from './html.js'; function hidden() { target(); }</script>\n"},
|
||||
{CBM_LANG_ASTRO, "Control.astro",
|
||||
"---\nimport value from './astro.js'; function hidden() { target(); }\n---\n"},
|
||||
{CBM_LANG_SVELTE, "Sibling.svelte",
|
||||
"<script>import value from './svelte.js'; function visible() { target(); }</script>\n"},
|
||||
{CBM_LANG_HTML, "sibling.html",
|
||||
"<script>import value from './html.js'; function visible() { target(); }</script>\n"},
|
||||
{CBM_LANG_ASTRO, "Sibling.astro",
|
||||
"---\nimport value from './astro.js'; function visible() { target(); }\n---\n"},
|
||||
};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
CBMFileResult *r = extract(hosts[i].source, hosts[i].language, "t", hosts[i].path);
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_EQ(count_defs_with_label(r, "Module"), 1);
|
||||
ASSERT_EQ(count_defs_with_label(r, "Function"), 0);
|
||||
ASSERT_EQ(r->calls.count, 0);
|
||||
ASSERT_EQ(count_defs_named(r, "Function", "visible"), 1);
|
||||
ASSERT_EQ(count_calls_named(r, "target"), 1);
|
||||
ASSERT_EQ(r->imports.count, 1);
|
||||
cbm_free_result(r);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Blocks that must never yield inline symbols, whatever the host: an external
|
||||
* program (src=) and a non-JavaScript MIME type. The bodies are deliberately
|
||||
* code, so a leak would surface as a definition and a call. HTML's own tag
|
||||
* walker still records the src= reference as an import; that edge names the
|
||||
* external file and is not the inline body leaking through. */
|
||||
TEST(embedded_structure_inert_blocks_issue1807) {
|
||||
static const struct {
|
||||
CBMLanguage language;
|
||||
const char *path;
|
||||
const char *source;
|
||||
int imports;
|
||||
} blocks[] = {
|
||||
{CBM_LANG_VUE, "Inert.vue",
|
||||
"<script type=\"application/json\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_VUE, "Inert.vue",
|
||||
"<script src=\"./x.js\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_SVELTE, "Inert.svelte",
|
||||
"<script type=\"application/json\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_SVELTE, "Inert.svelte",
|
||||
"<script src=\"./x.js\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_HTML, "inert.html",
|
||||
"<script type=\"application/json\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_HTML, "inert.html",
|
||||
"<script type=\"importmap\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_HTML, "inert.html",
|
||||
"<script type=\"text/x-template\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_HTML, "inert.html",
|
||||
"<script src=\"./x.js\">function hidden() { target(); }</script>\n", 1},
|
||||
{CBM_LANG_ASTRO, "Inert.astro",
|
||||
"<script type=\"application/json\">function hidden() { target(); }</script>\n", 0},
|
||||
{CBM_LANG_ASTRO, "Inert.astro",
|
||||
"<script src=\"./x.js\">function hidden() { target(); }</script>\n", 0},
|
||||
};
|
||||
for (int i = 0; i < 10; i++) {
|
||||
CBMFileResult *r = extract(blocks[i].source, blocks[i].language, "t", blocks[i].path);
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_EQ(count_defs_with_label(r, "Module"), 1);
|
||||
ASSERT_EQ(count_defs_with_label(r, "Function"), 0);
|
||||
ASSERT_EQ(r->calls.count, 0);
|
||||
ASSERT_EQ(r->imports.count, blocks[i].imports);
|
||||
cbm_free_result(r);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Svelte's module-level block (<script context="module">, or <script module>
|
||||
* since Svelte 5) is a second block in the same file: both contribute, each
|
||||
* honouring its own lang=, in host-file coordinates. */
|
||||
TEST(svelte_embedded_structure_both_blocks_issue1807) {
|
||||
static const char *sources[] = {
|
||||
"<script context=\"module\" lang=\"ts\">\n"
|
||||
"export function fromModule(): number { return shared(); }\n"
|
||||
"</script>\n"
|
||||
"<script lang=\"ts\">\n"
|
||||
"import { shared } from './shared';\n"
|
||||
"function fromInstance(): number { return shared() + fromModule(); }\n"
|
||||
"</script>\n"
|
||||
"<button on:click={fromInstance}>{fromModule()}</button>\n",
|
||||
"<script module lang=\"ts\">\n"
|
||||
"export function fromModule(): number { return shared(); }\n"
|
||||
"</script>\n"
|
||||
"<script lang=\"ts\">\n"
|
||||
"import { shared } from './shared';\n"
|
||||
"function fromInstance(): number { return shared() + fromModule(); }\n"
|
||||
"</script>\n"
|
||||
"<button onclick={fromInstance}>{fromModule()}</button>\n",
|
||||
};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
CBMFileResult *r = extract(sources[i], CBM_LANG_SVELTE, "t", "Widget.svelte");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_EQ(count_defs_with_label(r, "Function"), 2);
|
||||
ASSERT_EQ(count_defs_named(r, "Function", "fromModule"), 1);
|
||||
ASSERT_EQ(count_defs_named(r, "Function", "fromInstance"), 1);
|
||||
ASSERT_EQ(count_calls_named(r, "shared"), 2);
|
||||
ASSERT_EQ(count_calls_named(r, "fromModule"), 1);
|
||||
ASSERT_EQ(r->imports.count, 1);
|
||||
ASSERT(has_import(r, "shared"));
|
||||
for (int d = 0; d < r->defs.count; d++) {
|
||||
const CBMDefinition *def = &r->defs.items[d];
|
||||
if (strcmp(def->name, "fromModule") == 0) {
|
||||
ASSERT_EQ(def->start_line, 2);
|
||||
} else if (strcmp(def->name, "fromInstance") == 0) {
|
||||
ASSERT_EQ(def->start_line, 6);
|
||||
}
|
||||
}
|
||||
cbm_free_result(r);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Every type= form HTML itself runs as JavaScript contributes, in host-file
|
||||
* coordinates; a data block on the same page does not. */
|
||||
TEST(html_embedded_structure_issue1807) {
|
||||
CBMFileResult *r =
|
||||
extract("<!DOCTYPE html><html><head>\n"
|
||||
"<script type=\"module\">\n"
|
||||
"import { renderApp } from './app.js';\n"
|
||||
"function boot() { renderApp(); }\n"
|
||||
"</script>\n"
|
||||
"<script type=\"text/javascript\">\n"
|
||||
"function legacy() { boot(); }\n"
|
||||
"</script>\n"
|
||||
"<script type=\"application/javascript\">\n"
|
||||
"function fallback() { legacy(); }\n"
|
||||
"</script>\n"
|
||||
"<script type=\"application/ld+json\">{\"@type\": \"Thing\"}</script>\n"
|
||||
"</head><body></body></html>\n",
|
||||
CBM_LANG_HTML, "t", "index.html");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_EQ(count_defs_with_label(r, "Function"), 3);
|
||||
ASSERT_EQ(count_calls_named(r, "renderApp"), 1);
|
||||
ASSERT_EQ(count_calls_named(r, "boot"), 1);
|
||||
ASSERT_EQ(count_calls_named(r, "legacy"), 1);
|
||||
ASSERT_EQ(r->imports.count, 1);
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const CBMDefinition *d = &r->defs.items[i];
|
||||
if (strcmp(d->name, "boot") == 0) {
|
||||
ASSERT_EQ(d->start_line, 4);
|
||||
} else if (strcmp(d->name, "legacy") == 0) {
|
||||
ASSERT_EQ(d->start_line, 7);
|
||||
} else if (strcmp(d->name, "fallback") == 0) {
|
||||
ASSERT_EQ(d->start_line, 10);
|
||||
}
|
||||
}
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Astro's frontmatter fence and <script> bodies are TypeScript by default: an
|
||||
* interface and typed signatures parse, and both blocks contribute in
|
||||
* host-file coordinates. */
|
||||
TEST(astro_embedded_structure_issue1807) {
|
||||
CBMFileResult *r = extract("---\n"
|
||||
"import Header from './Header.astro';\n"
|
||||
"interface Props { title: string }\n"
|
||||
"const { title }: Props = Astro.props;\n"
|
||||
"function heading(): string { return format(title); }\n"
|
||||
"---\n"
|
||||
"<Header />\n"
|
||||
"<script>\n"
|
||||
"function hydrate(): void { heading(); }\n"
|
||||
"</script>\n",
|
||||
CBM_LANG_ASTRO, "t", "Page.astro");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT(has_def_any(r, "Props"));
|
||||
ASSERT_EQ(count_defs_named(r, "Function", "heading"), 1);
|
||||
ASSERT_EQ(count_defs_named(r, "Function", "hydrate"), 1);
|
||||
ASSERT_EQ(count_calls_named(r, "format"), 1);
|
||||
ASSERT_EQ(count_calls_named(r, "heading"), 1);
|
||||
ASSERT_EQ(r->imports.count, 1);
|
||||
ASSERT(has_import(r, "Header.astro"));
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const CBMDefinition *d = &r->defs.items[i];
|
||||
if (strcmp(d->name, "heading") == 0) {
|
||||
ASSERT_EQ(d->start_line, 5);
|
||||
} else if (strcmp(d->name, "hydrate") == 0) {
|
||||
ASSERT_EQ(d->start_line, 9);
|
||||
}
|
||||
}
|
||||
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"
|
||||
@@ -7235,7 +7405,11 @@ SUITE(extraction) {
|
||||
RUN_TEST(vue_imports_basic);
|
||||
RUN_TEST(vue_embedded_structure_issue1410);
|
||||
RUN_TEST(vue_embedded_structure_negative_controls_issue1410);
|
||||
RUN_TEST(vue_embedded_structure_host_controls_issue1410);
|
||||
RUN_TEST(embedded_structure_sibling_hosts_issue1807);
|
||||
RUN_TEST(embedded_structure_inert_blocks_issue1807);
|
||||
RUN_TEST(svelte_embedded_structure_both_blocks_issue1807);
|
||||
RUN_TEST(html_embedded_structure_issue1807);
|
||||
RUN_TEST(astro_embedded_structure_issue1807);
|
||||
RUN_TEST(html_imports_basic);
|
||||
|
||||
/* config_extraction_test.go ports */
|
||||
|
||||
@@ -236,8 +236,10 @@ static const LabelGolden LABEL_GOLDENS[] = {
|
||||
{"liquid", "Module:1"},
|
||||
{"blade", "Module:1"},
|
||||
{"vue", "Module:1"},
|
||||
{"svelte", "Module:1"},
|
||||
{"astro", "Module:1"},
|
||||
/* Svelte's <script> and Astro's frontmatter fence extract structure like a
|
||||
* standalone script, so each fixture's one declaration is a Variable. */
|
||||
{"svelte", "Module:1,Variable:1"},
|
||||
{"astro", "Module:1,Variable:1"},
|
||||
{"templ", "Class:1,Module:1"},
|
||||
{"typst", "Module:1"},
|
||||
{"mermaid", "Module:1"},
|
||||
|
||||
Reference in New Issue
Block a user