Skip to content

Commit e678722

Browse files
authored
Merge pull request DeusData#1852 from DeusData/fix/vue-embedded-structure
fix(extract): extract structure from Vue inline scripts
2 parents 3cda5b1 + 5a02822 commit e678722

5 files changed

Lines changed: 307 additions & 38 deletions

File tree

‎internal/cbm/cbm.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,9 @@ void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);
728728
// --- Sub-extractor entry points ---
729729

730730
void cbm_extract_definitions(CBMExtractCtx *ctx);
731+
/* Internal companion for embedded-language trees that contribute definitions
732+
* to an existing host-file Module rather than minting a second Module. */
733+
void cbm_extract_definitions_without_module(CBMExtractCtx *ctx);
731734
// dbt lineage for Jinja-templated SQL models: emits a Model def plus one usage
732735
// per ref()/source() call. No-op unless the file parses as SQL and actually
733736
// contains a dbt builtin call. Defined in extract_dbt.c.

‎internal/cbm/extract_defs.c‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7533,6 +7533,19 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
75337533
free(s.data);
75347534
}
75357535

7536+
void cbm_extract_definitions_without_module(CBMExtractCtx *ctx) {
7537+
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
7538+
if (!spec) {
7539+
return;
7540+
}
7541+
7542+
// Walk AST for function/class definitions
7543+
walk_defs(ctx, ctx->root, spec, 0);
7544+
7545+
// Extract module-level variables
7546+
extract_variables(ctx, ctx->root, spec);
7547+
}
7548+
75367549
void cbm_extract_definitions(CBMExtractCtx *ctx) {
75377550
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
75387551
if (!spec) {
@@ -7554,9 +7567,5 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
75547567
mod.is_test = ctx->result->is_test_file;
75557568
cbm_defs_push(&ctx->result->defs, a, mod);
75567569

7557-
// Walk AST for function/class definitions
7558-
walk_defs(ctx, ctx->root, spec, 0);
7559-
7560-
// Extract module-level variables
7561-
extract_variables(ctx, ctx->root, spec);
7570+
cbm_extract_definitions_without_module(ctx);
75627571
}

‎internal/cbm/extract_imports.c‎

Lines changed: 155 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,8 +1395,13 @@ static void parse_spec_imports(CBMExtractCtx *ctx) {
13951395
// that the main parser uses. Adding another host language is a one-line
13961396
// declaration in lang_specs.c.
13971397

1398+
typedef struct {
1399+
TSNode script;
1400+
TSNode content;
1401+
} CBMEmbeddedBlock;
1402+
13981403
static void embedded_collect_content_nodes(TSNode root, const CBMEmbeddedLangSpec *spec,
1399-
TSNode *out, int *out_count, int max_out) {
1404+
CBMEmbeddedBlock *out, int *out_count, int max_out) {
14001405
/* Iterative DFS so deeply-nested script blocks are still found. Cap the
14011406
* stack to a sane bound (host grammars do not have million-deep markup
14021407
* trees) — no need to introduce TSNodeStack here. */
@@ -1412,7 +1417,8 @@ static void embedded_collect_content_nodes(TSNode root, const CBMEmbeddedLangSpe
14121417
for (uint32_t k = 0; k < cc; k++) {
14131418
TSNode c = ts_node_child(node, k);
14141419
if (strcmp(ts_node_type(c), spec->content_node_type) == 0) {
1415-
out[(*out_count)++] = c;
1420+
out[*out_count] = (CBMEmbeddedBlock){.script = node, .content = c};
1421+
(*out_count)++;
14161422
if (*out_count >= max_out) {
14171423
return;
14181424
}
@@ -1429,50 +1435,166 @@ static void embedded_collect_content_nodes(TSNode root, const CBMEmbeddedLangSpe
14291435
}
14301436
}
14311437

1438+
static bool ascii_trimmed_equals(const char *value, const char *expected) {
1439+
if (!value || !expected) {
1440+
return false;
1441+
}
1442+
while (*value && isspace((unsigned char)*value)) {
1443+
value++;
1444+
}
1445+
size_t len = strlen(value);
1446+
while (len > 0 && isspace((unsigned char)value[len - 1])) {
1447+
len--;
1448+
}
1449+
size_t expected_len = strlen(expected);
1450+
if (len != expected_len) {
1451+
return false;
1452+
}
1453+
for (size_t i = 0; i < len; i++) {
1454+
if (tolower((unsigned char)value[i]) != tolower((unsigned char)expected[i])) {
1455+
return false;
1456+
}
1457+
}
1458+
return true;
1459+
}
1460+
1461+
/* Vue is the only host that opts into structural embedded extraction. Restrict
1462+
* its inline programs to the language forms whose parsers we can select
1463+
* exactly. A src attribute always denotes an external program and therefore
1464+
* suppresses any inline extraction, even for malformed mixed markup. */
1465+
static bool vue_embedded_language(CBMExtractCtx *ctx, TSNode script, CBMLanguage *language) {
1466+
enum { VUE_ATTR_STACK_CAP = 128 };
1467+
TSNode stack[VUE_ATTR_STACK_CAP];
1468+
int top = 0;
1469+
bool has_lang = false;
1470+
bool lang_supported = true;
1471+
CBMLanguage selected = CBM_LANG_JAVASCRIPT;
1472+
stack[top++] = script;
1473+
while (top > 0) {
1474+
TSNode node = stack[--top];
1475+
if (strcmp(ts_node_type(node), "attribute") == 0) {
1476+
uint32_t named_count = ts_node_named_child_count(node);
1477+
if (named_count == 0) {
1478+
return false;
1479+
}
1480+
TSNode name_node = ts_node_named_child(node, 0);
1481+
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
1482+
if (ascii_trimmed_equals(name, "src")) {
1483+
return false;
1484+
}
1485+
if (ascii_trimmed_equals(name, "lang")) {
1486+
TSNode value_node = node;
1487+
if (!find_first_descendant_of(node, "attribute_value", &value_node)) {
1488+
return false;
1489+
}
1490+
char *value = cbm_node_text(ctx->arena, value_node, ctx->source);
1491+
has_lang = true;
1492+
if (ascii_trimmed_equals(value, "js") ||
1493+
ascii_trimmed_equals(value, "javascript")) {
1494+
selected = CBM_LANG_JAVASCRIPT;
1495+
} else if (ascii_trimmed_equals(value, "ts") ||
1496+
ascii_trimmed_equals(value, "typescript")) {
1497+
selected = CBM_LANG_TYPESCRIPT;
1498+
} else {
1499+
lang_supported = false;
1500+
}
1501+
}
1502+
continue;
1503+
}
1504+
uint32_t count = ts_node_named_child_count(node);
1505+
if ((int)count > VUE_ATTR_STACK_CAP - top) {
1506+
return false;
1507+
}
1508+
for (int i = (int)count - 1; i >= 0; i--) {
1509+
stack[top++] = ts_node_named_child(node, (uint32_t)i);
1510+
}
1511+
}
1512+
if (has_lang && !lang_supported) {
1513+
return false;
1514+
}
1515+
*language = selected;
1516+
return true;
1517+
}
1518+
1519+
static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock *block,
1520+
CBMLanguage embedded_language, bool extract_structure) {
1521+
const TSLanguage *language = cbm_ts_language(embedded_language);
1522+
if (!language || ctx->source_len < 0) {
1523+
return;
1524+
}
1525+
TSRange range = {
1526+
.start_point = ts_node_start_point(block->content),
1527+
.end_point = ts_node_end_point(block->content),
1528+
.start_byte = ts_node_start_byte(block->content),
1529+
.end_byte = ts_node_end_byte(block->content),
1530+
};
1531+
if (range.end_byte <= range.start_byte || range.end_byte > (uint32_t)ctx->source_len) {
1532+
return;
1533+
}
1534+
1535+
/* One fresh parser/tree per block prevents parser state or error recovery
1536+
* from one script from changing the next script's result. Parsing the full
1537+
* original buffer through the absolute included range preserves byte and
1538+
* point coordinates across CRLF and UTF-8 prefixes. */
1539+
TSParser *parser = ts_parser_new();
1540+
if (!parser) {
1541+
return;
1542+
}
1543+
if (!ts_parser_set_language(parser, language) ||
1544+
!ts_parser_set_included_ranges(parser, &range, 1)) {
1545+
ts_parser_delete(parser);
1546+
return;
1547+
}
1548+
TSTree *tree = ts_parser_parse_string(parser, NULL, ctx->source, (uint32_t)ctx->source_len);
1549+
if (!tree) {
1550+
ts_parser_delete(parser);
1551+
return;
1552+
}
1553+
1554+
CBMExtractCtx sub_ctx = {
1555+
.arena = ctx->arena,
1556+
.result = ctx->result,
1557+
.source = ctx->source,
1558+
.source_len = ctx->source_len,
1559+
.language = embedded_language,
1560+
.project = ctx->project,
1561+
.rel_path = ctx->rel_path,
1562+
.module_qn = ctx->module_qn,
1563+
.root = ts_tree_root_node(tree),
1564+
.macro_table = ctx->macro_table,
1565+
.return_type_table = ctx->return_type_table,
1566+
};
1567+
if (extract_structure) {
1568+
cbm_extract_definitions_without_module(&sub_ctx);
1569+
}
1570+
walk_es_imports(&sub_ctx, sub_ctx.root);
1571+
if (extract_structure) {
1572+
cbm_extract_unified(&sub_ctx);
1573+
}
1574+
1575+
ts_tree_delete(tree);
1576+
ts_parser_delete(parser);
1577+
}
1578+
14321579
static void parse_embedded_imports(CBMExtractCtx *ctx) {
14331580
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
14341581
if (!spec || !spec->embedded_imports) {
14351582
return;
14361583
}
14371584
for (const CBMEmbeddedLangSpec *e = spec->embedded_imports; e->script_node_type != NULL; e++) {
1438-
const TSLanguage *embedded_lang = cbm_ts_language(e->embedded_language);
1439-
if (!embedded_lang) {
1440-
continue; /* embedded grammar not linked in — silently skip */
1441-
}
14421585
enum { MAX_EMBEDDED_BLOCKS = 16 };
1443-
TSNode hits[MAX_EMBEDDED_BLOCKS];
1586+
CBMEmbeddedBlock hits[MAX_EMBEDDED_BLOCKS];
14441587
int hit_count = 0;
14451588
embedded_collect_content_nodes(ctx->root, e, hits, &hit_count, MAX_EMBEDDED_BLOCKS);
1446-
if (hit_count == 0) {
1447-
continue;
1448-
}
1449-
TSParser *parser = ts_parser_new();
1450-
if (!parser) {
1451-
continue;
1452-
}
1453-
if (!ts_parser_set_language(parser, embedded_lang)) {
1454-
ts_parser_delete(parser);
1455-
continue;
1456-
}
14571589
for (int i = 0; i < hit_count; i++) {
1458-
uint32_t s = ts_node_start_byte(hits[i]);
1459-
uint32_t end = ts_node_end_byte(hits[i]);
1460-
if (end <= s) {
1590+
CBMLanguage embedded_language = e->embedded_language;
1591+
bool extract_structure = ctx->language == CBM_LANG_VUE;
1592+
if (extract_structure &&
1593+
!vue_embedded_language(ctx, hits[i].script, &embedded_language)) {
14611594
continue;
14621595
}
1463-
const char *sub_src = ctx->source + s;
1464-
uint32_t sub_len = end - s;
1465-
TSTree *sub_tree = ts_parser_parse_string(parser, NULL, sub_src, sub_len);
1466-
if (!sub_tree) {
1467-
continue;
1468-
}
1469-
CBMExtractCtx sub_ctx = *ctx;
1470-
sub_ctx.source = sub_src;
1471-
sub_ctx.root = ts_tree_root_node(sub_tree);
1472-
walk_es_imports(&sub_ctx, sub_ctx.root);
1473-
ts_tree_delete(sub_tree);
1596+
parse_one_embedded_block(ctx, &hits[i], embedded_language, extract_structure);
14741597
}
1475-
ts_parser_delete(parser);
14761598
}
14771599
}
14781600

‎tests/test_edge_structural.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,19 @@ TEST(es_calls_crossfile_typescript) {
407407
PASS();
408408
}
409409

410+
TEST(es_calls_vue_embedded_issue1410) {
411+
static const ES_LangFile f[] = {
412+
{"App.vue",
413+
"<template><p>Vue</p></template>\n"
414+
"<script setup lang=\"ts\">\n"
415+
"function callee(): number { return 42; }\n"
416+
"function caller(): number { return callee(); }\n"
417+
"</script>\n"},
418+
};
419+
ASSERT_EQ(es_exact_edge_by_name(f, 1, "CALLS", "caller", "callee"), 1);
420+
PASS();
421+
}
422+
410423
/* Java: caller in Main.java calls static method from Util.java (same package). */
411424
TEST(es_calls_crossfile_java) {
412425
static const ES_LangFile f[] = {
@@ -913,6 +926,7 @@ SUITE(edge_structural) {
913926
RUN_TEST(es_calls_crossfile_java);
914927
RUN_TEST(es_calls_crossfile_kotlin);
915928
RUN_TEST(es_calls_crossfile_csharp);
929+
RUN_TEST(es_calls_vue_embedded_issue1410);
916930

917931
/* ── FAMILY 2: INHERITS cross-file ────────────────────────── */
918932
/* GREEN: Java, C#, C++ (extraction confirmed correct). */

0 commit comments

Comments
 (0)