Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 80 additions & 66 deletions internal/cbm/extract_imports.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
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")) {
if (!apply_script_attribute(ctx, node, language)) {
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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
}
Expand Down
15 changes: 9 additions & 6 deletions internal/cbm/lang_specs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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},
};

Expand Down
14 changes: 14 additions & 0 deletions tests/test_edge_structural.c
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = {
Expand Down Expand Up @@ -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). */
Expand Down
Loading
Loading