Skip to content

Commit 38e6dd0

Browse files
feat(lang): CFML tag-dialect .cfc routing + embedded <cfscript> defs
Two extraction fixes for legacy tag-based CFML/ColdFusion, where the vendored cfmleditor grammar (Lucee-tuned, script-first) leaves most real-world components unextracted. 1. .cfc dialect disambiguation (cbm_disambiguate_cfc) The extension table maps .cfc -> cfscript unconditionally, but legacy codebases are predominantly tag-based (<cfcomponent>). Content-sniff the file head, mirroring cbm_disambiguate_cls/inc: <cfcomponent>/<cffunction> route to the cfml tag grammar; a leading <cfscript>-wrapped "component {}" or a plain script component route to cfscript; leading <!--- ---> comments are skipped. 2. Embedded <cfscript> definition extraction (cbm_extract_embedded_defs) The HTML-derived cfml grammar keeps <cfscript> bodies as opaque cf_script_content, so script functions inside tag components were never extracted. Re-parse cf_script_tag -> cf_script_content slices with the cfscript grammar (reusing the embedded-imports machinery) and walk them for definitions via a new cbm_extract_definitions_body() that skips the Module node. Block-relative line numbers are shifted by the block's start row; CBMDefinition carries only line positions, so a constant offset is a complete remap. Definitions only (calls/usages would need byte remapping), so existing edges cannot be corrupted. As a bonus, functions are recovered even when a <cfsetting> void-tag ERROR cascade damages the surrounding tree. The opt-in is an extract_definitions flag on CBMEmbeddedLangSpec (carried by CFML's embedded_imports entry), not a new CBMLangSpec field -- the latter would trip -Wmissing-field-initializers across every language row under clang -Werror. Tests: extract_cfml_embedded_cfscript_defs (extraction + line remap) and eight lang_cfc_* disambiguation cases. Verified on a ~700-file legacy CFML codebase: parse timeouts 4->0, api.cfc 0->62 functions, +1,532 function nodes overall, with exact line numbers. Signed-off-by: Allan Oepping <allan@intellipay.com>
1 parent dfe67cc commit 38e6dd0

11 files changed

Lines changed: 357 additions & 32 deletions

File tree

‎internal/cbm/cbm.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
12921292
// Run extractors: defs + imports use separate walks (unique recursion patterns),
12931293
// then a single unified cursor walk handles the remaining 7 extractors.
12941294
cbm_extract_definitions(&ctx);
1295+
cbm_extract_embedded_defs(&ctx); // defs inside <cfscript> blocks of CFML tag files
12951296
cbm_extract_imports(&ctx);
12961297
cbm_extract_unified(&ctx);
12971298

‎internal/cbm/cbm.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,8 @@ void cbm_extract_definitions(CBMExtractCtx *ctx);
730730
// per ref()/source() call. No-op unless the file parses as SQL and actually
731731
// contains a dbt builtin call. Defined in extract_dbt.c.
732732
void cbm_extract_dbt(CBMExtractCtx *ctx);
733+
void cbm_extract_definitions_body(CBMExtractCtx *ctx); // no Module node (for sub-trees)
734+
void cbm_extract_embedded_defs(CBMExtractCtx *ctx); // re-parse embedded script blocks for defs
733735
void cbm_extract_imports(CBMExtractCtx *ctx);
734736
void cbm_extract_usages(CBMExtractCtx *ctx);
735737
void cbm_extract_semantic(CBMExtractCtx *ctx);

‎internal/cbm/extract_defs.c‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7524,6 +7524,20 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
75247524
free(s.data);
75257525
}
75267526

7527+
// Walk the tree for functions/classes/fields and module-level variables, WITHOUT
7528+
// emitting the file-level Module node. Split out so embedded sub-trees (e.g. a
7529+
// CFML <cfscript> block re-parsed with the cfscript grammar) can be walked for
7530+
// definitions without minting a spurious Module per block. See
7531+
// cbm_extract_embedded_defs().
7532+
void cbm_extract_definitions_body(CBMExtractCtx *ctx) {
7533+
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
7534+
if (!spec) {
7535+
return;
7536+
}
7537+
walk_defs(ctx, ctx->root, spec, 0);
7538+
extract_variables(ctx, ctx->root, spec);
7539+
}
7540+
75277541
void cbm_extract_definitions(CBMExtractCtx *ctx) {
75287542
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
75297543
if (!spec) {
@@ -7545,9 +7559,6 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
75457559
mod.is_test = ctx->result->is_test_file;
75467560
cbm_defs_push(&ctx->result->defs, a, mod);
75477561

7548-
// Walk AST for function/class definitions
7549-
walk_defs(ctx, ctx->root, spec, 0);
7550-
7551-
// Extract module-level variables
7552-
extract_variables(ctx, ctx->root, spec);
7562+
// Walk AST for definitions + module-level variables
7563+
cbm_extract_definitions_body(ctx);
75537564
}

‎internal/cbm/extract_imports.c‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,78 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
14761476
}
14771477
}
14781478

1479+
// Re-parse embedded script blocks flagged extract_definitions in embedded_imports
1480+
// and walk them for DEFINITIONS, not imports. Mirrors parse_embedded_imports but
1481+
// runs cbm_extract_definitions_body on each inner AST. Because the inner tree is
1482+
// parsed from a slice starting at the block, its node rows are block-relative;
1483+
// after extracting, we shift the newly-added definitions' line numbers by the
1484+
// block's start row so they map back to the host file. CBMDefinition carries
1485+
// only line positions (no byte offsets), so a constant row shift is a complete
1486+
// remap. Definitions only — calls/usages inside the block are intentionally not
1487+
// walked here (they would need byte remapping too and could pollute the call
1488+
// graph), so this cannot corrupt existing edges.
1489+
void cbm_extract_embedded_defs(CBMExtractCtx *ctx) {
1490+
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
1491+
if (!spec || !spec->embedded_imports) {
1492+
return;
1493+
}
1494+
for (const CBMEmbeddedLangSpec *e = spec->embedded_imports; e->script_node_type != NULL; e++) {
1495+
if (!e->extract_definitions) {
1496+
continue; /* this embedded block is walked for imports only */
1497+
}
1498+
const TSLanguage *embedded_lang = cbm_ts_language(e->embedded_language);
1499+
if (!embedded_lang) {
1500+
continue; /* embedded grammar not linked in — silently skip */
1501+
}
1502+
enum { MAX_EMBEDDED_BLOCKS = 64 };
1503+
TSNode hits[MAX_EMBEDDED_BLOCKS];
1504+
int hit_count = 0;
1505+
embedded_collect_content_nodes(ctx->root, e, hits, &hit_count, MAX_EMBEDDED_BLOCKS);
1506+
if (hit_count == 0) {
1507+
continue;
1508+
}
1509+
TSParser *parser = ts_parser_new();
1510+
if (!parser) {
1511+
continue;
1512+
}
1513+
if (!ts_parser_set_language(parser, embedded_lang)) {
1514+
ts_parser_delete(parser);
1515+
continue;
1516+
}
1517+
for (int i = 0; i < hit_count; i++) {
1518+
uint32_t s = ts_node_start_byte(hits[i]);
1519+
uint32_t end = ts_node_end_byte(hits[i]);
1520+
if (end <= s) {
1521+
continue;
1522+
}
1523+
const char *sub_src = ctx->source + s;
1524+
uint32_t sub_len = end - s;
1525+
TSTree *sub_tree = ts_parser_parse_string(parser, NULL, sub_src, sub_len);
1526+
if (!sub_tree) {
1527+
continue;
1528+
}
1529+
CBMExtractCtx sub_ctx = *ctx;
1530+
sub_ctx.source = sub_src;
1531+
sub_ctx.source_len = (int)sub_len;
1532+
sub_ctx.language = e->embedded_language;
1533+
sub_ctx.root = ts_tree_root_node(sub_tree);
1534+
1535+
int defs_before = ctx->result->defs.count;
1536+
cbm_extract_definitions_body(&sub_ctx);
1537+
1538+
/* Shift block-relative line numbers back to host-file lines. */
1539+
uint32_t row0 = ts_node_start_point(hits[i]).row;
1540+
for (int j = defs_before; j < ctx->result->defs.count; j++) {
1541+
CBMDefinition *d = &ctx->result->defs.items[j];
1542+
d->start_line += row0;
1543+
d->end_line += row0;
1544+
}
1545+
ts_tree_delete(sub_tree);
1546+
}
1547+
ts_parser_delete(parser);
1548+
}
1549+
}
1550+
14791551
// --- Namespace / package declaration capture ---
14801552
// Java/Kotlin/C#/PHP put the file's symbols inside a namespace/package whose
14811553
// name is NOT reflected in the path-based QN scheme. Capturing it lets the

‎internal/cbm/lang_specs.c‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,16 @@ static const char *cfml_branch_types[] = {
287287
"cf_if_tag", "cf_elseif_tag", "cf_else_tag", "if_statement",
288288
"for_statement", "while_statement", "switch_statement", NULL};
289289
static const char *cfml_module_types[] = {"program", "component_file", NULL};
290+
// The cfml (HTML-derived) grammar keeps the body of a <cfscript> block as an
291+
// opaque cf_script_content token — it does NOT parse the script-dialect
292+
// functions inside. Re-parse that slice with the cfscript grammar (extract_
293+
// definitions = true) so those functions become real definitions. (Contrast the
294+
// comment above: embedded <cfscript> functions only "appear as function_
295+
// declaration" once re-parsed here; in the raw cfml tree they are unparsed text.)
296+
static const CBMEmbeddedLangSpec cfml_embedded_imports[] = {
297+
{"cf_script_tag", "cf_script_content", CBM_LANG_CFSCRIPT, true},
298+
{NULL, NULL, 0, false},
299+
};
290300

291301
// ==================== RUST ====================
292302
static const char *rust_func_types[] = {"function_item", "function_signature_item",
@@ -874,24 +884,24 @@ static const char *graphql_field_types[] = {"field_definition", "input_value_def
874884
// so the existing ES import extractor sees real import_statement nodes.
875885
// Terminator: an entry whose script_node_type is NULL.
876886
static const CBMEmbeddedLangSpec vue_embedded_imports[] = {
877-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
878-
{NULL, NULL, 0},
887+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
888+
{NULL, NULL, 0, false},
879889
};
880890
static const CBMEmbeddedLangSpec svelte_embedded_imports[] = {
881-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
882-
{NULL, NULL, 0},
891+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
892+
{NULL, NULL, 0, false},
883893
};
884894
static const CBMEmbeddedLangSpec html_embedded_imports[] = {
885-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
886-
{NULL, NULL, 0},
895+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
896+
{NULL, NULL, 0, false},
887897
};
888898
static const CBMEmbeddedLangSpec astro_embedded_imports[] = {
889899
/* Astro component scripts live in the `---` frontmatter fence, which the
890900
* grammar keeps as an unparsed frontmatter_js_block. Re-parse that slice
891901
* with the JS grammar so `import X from './X.astro'` becomes a real edge. */
892-
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT},
893-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
894-
{NULL, NULL, 0},
902+
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT, false},
903+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
904+
{NULL, NULL, 0, false},
895905
};
896906

897907
// ==================== VUE ====================
@@ -2077,7 +2087,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
20772087
[CBM_LANG_CFML] = {CBM_LANG_CFML, cfml_func_types, empty_types, empty_types, cfml_module_types,
20782088
cfml_call_types, empty_types, empty_types, cfml_branch_types, empty_types,
20792089
empty_types, empty_types, NULL, empty_types, NULL, NULL, tree_sitter_cfml,
2080-
NULL},
2090+
cfml_embedded_imports},
20812091

20822092
// CBM_LANG_GLEAM
20832093
[CBM_LANG_GLEAM] = {CBM_LANG_GLEAM, gleam_func_types, gleam_class_types, gleam_field_types,

‎internal/cbm/lang_specs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ typedef struct {
1414
const char *script_node_type; // e.g. "script_element"
1515
const char *content_node_type; // e.g. "raw_text"
1616
CBMLanguage embedded_language; // grammar used to re-parse the content slice
17+
// When true, the re-parsed inner AST is also walked for DEFINITIONS, not just
18+
// imports — e.g. CFML tag components whose <cfscript> body (cf_script_tag ->
19+
// cf_script_content) holds script-dialect functions the HTML-derived cfml
20+
// grammar keeps as opaque content. Kept on this small struct (a handful of
21+
// instances) rather than as a CBMLangSpec field, which would trip
22+
// -Wmissing-field-initializers across every language row.
23+
bool extract_definitions;
1724
} CBMEmbeddedLangSpec;
1825

1926
// CBMLangSpec mirrors Go's lang.LanguageSpec with NULL-terminated string arrays.

‎src/discover/discover.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,10 @@ static CBMLanguage detect_file_language(const char *entry_name, const char *abs_
708708
if (dot && strcmp(dot, ".inc") == 0) {
709709
lang = cbm_disambiguate_inc(abs_path);
710710
}
711+
/* Special: .cfc components may be script-dialect or tag-dialect (<cfcomponent>) */
712+
if (dot && strcmp(dot, ".cfc") == 0) {
713+
lang = cbm_disambiguate_cfc(abs_path);
714+
}
711715
/* Special: ObjectScript Studio Export XML (<Export generator="...">) is
712716
* detected by content; otherwise .xml stays XML. */
713717
if (lang == CBM_LANG_XML) {

‎src/discover/discover.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ CBMLanguage cbm_disambiguate_inc(const char *path);
6464
* shebang, an embedded NUL in the first line, or an unknown interpreter. */
6565
CBMLanguage cbm_language_from_shebang(const char *path);
6666

67+
/* Disambiguate .cfc files by reading the head of the content.
68+
* Returns CBM_LANG_CFML if the component is tag-based (a "<cfcomponent" tag, or a
69+
* leading '<'), otherwise CBM_LANG_CFSCRIPT for script-dialect components.
70+
* On read failure, defaults to CBM_LANG_CFSCRIPT. */
71+
CBMLanguage cbm_disambiguate_cfc(const char *path);
72+
6773
/* ── Gitignore pattern matching ──────────────────────────────────── */
6874

6975
typedef struct cbm_gitignore cbm_gitignore_t;

‎src/discover/language.c‎

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "cbm.h" // CBMLanguage, CBM_LANG_*
1212

1313
#include "foundation/constants.h"
14+
#include "foundation/compat.h" // cbm_strcasestr
1415
#include "foundation/compat_fs.h"
1516

1617
enum { LANG_SCAN_PASSES = 2 };
@@ -453,7 +454,10 @@ static const ext_entry_t EXT_TABLE[] = {
453454
/* Qt QML */
454455
{".qml", CBM_LANG_QML},
455456

456-
/* CFML / ColdFusion — .cfc components are script-dialect; .cfm are tag templates */
457+
/* CFML / ColdFusion — .cfm are tag templates; .cfc components may be EITHER
458+
* script-dialect (component { ... }) or tag-dialect (<cfcomponent> ...). The
459+
* table default is script; tag-based .cfc are resolved by content in
460+
* cbm_disambiguate_cfc(). */
457461
{".cfc", CBM_LANG_CFSCRIPT},
458462
{".cfm", CBM_LANG_CFML},
459463

@@ -1288,3 +1292,74 @@ CBMLanguage cbm_disambiguate_inc(const char *path) {
12881292
}
12891293
return CBM_LANG_BITBAKE;
12901294
}
1295+
1296+
/* Case-insensitive prefix match (portable — no strncasecmp dependency). */
1297+
static bool starts_with_ci(const char *s, const char *prefix) {
1298+
for (; *prefix; s++, prefix++) {
1299+
if (tolower((unsigned char)*s) != tolower((unsigned char)*prefix)) {
1300+
return false;
1301+
}
1302+
}
1303+
return true;
1304+
}
1305+
1306+
/* Disambiguate .cfc files: a ColdFusion component may be written in the script
1307+
* dialect ("component { ... }", parsed by the JS-like cfscript grammar) or the
1308+
* tag dialect ("<cfcomponent> ... <cffunction>", parsed by the HTML-derived cfml
1309+
* grammar). The extension table defaults to cfscript because that is what modern
1310+
* Lucee/ACF templates use, but large legacy codebases are predominantly tag-based
1311+
* and feeding those to the wrong grammar fails wholesale. Routing rules:
1312+
* 1. A "<cfcomponent" or top-level "<cffunction" tag ⇒ tag dialect. (The latter
1313+
* catches "bare" tag components that omit the <cfcomponent> wrapper.) This
1314+
* wins regardless of any leading <!---/<cfscript>, so it is checked first.
1315+
* 2. Otherwise the file is script-dialect content. Find the first significant
1316+
* token, skipping whitespace and <!--- ---> comments:
1317+
* - a leading "<cfscript>" wrapper is still script content ⇒ cfscript;
1318+
* - a different leading tag (e.g. <cfquery> in a bare-tag file) ⇒ cfml;
1319+
* - anything else ("component { ... }") ⇒ cfscript.
1320+
* Defaults to CBM_LANG_CFSCRIPT on any doubt (preserves table behaviour). */
1321+
CBMLanguage cbm_disambiguate_cfc(const char *path) {
1322+
if (!path) {
1323+
return CBM_LANG_CFSCRIPT;
1324+
}
1325+
1326+
FILE *f = cbm_fopen(path, "r");
1327+
if (!f) {
1328+
return CBM_LANG_CFSCRIPT;
1329+
}
1330+
1331+
/* Read a generous head: tag components can carry a large license/revision
1332+
* comment block before the <cfcomponent> opener. */
1333+
char buf[CBM_SZ_16K + SKIP_ONE];
1334+
size_t n = fread(buf, SKIP_ONE, CBM_SZ_16K, f);
1335+
buf[n] = '\0';
1336+
(void)fclose(f);
1337+
1338+
/* Rule 1: explicit tag-component markers ⇒ tag dialect. */
1339+
if (cbm_strcasestr(buf, "<cfcomponent") != NULL || cbm_strcasestr(buf, "<cffunction") != NULL) {
1340+
return CBM_LANG_CFML;
1341+
}
1342+
1343+
/* Rule 2: locate the first significant token, past whitespace and comments. */
1344+
const char *p = buf;
1345+
for (;;) {
1346+
while (*p && isspace((unsigned char)*p)) {
1347+
p++;
1348+
}
1349+
if (starts_with_ci(p, "<!---")) {
1350+
const char *end = strstr(p + SLEN("<!---"), "--->");
1351+
if (!end) {
1352+
break; /* comment runs past the buffer — treat as no token */
1353+
}
1354+
p = end + SLEN("--->");
1355+
continue;
1356+
}
1357+
break;
1358+
}
1359+
if (*p == '<') {
1360+
/* A leading <cfscript> wrapper is script content; any other leading tag
1361+
* (bare-tag file) is tag content. */
1362+
return starts_with_ci(p, "<cfscript") ? CBM_LANG_CFSCRIPT : CBM_LANG_CFML;
1363+
}
1364+
return CBM_LANG_CFSCRIPT;
1365+
}

0 commit comments

Comments
 (0)