Skip to content

Commit 88aab7a

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 24bee57 commit 88aab7a

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
@@ -1249,6 +1249,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
12491249
// Run extractors: defs + imports use separate walks (unique recursion patterns),
12501250
// then a single unified cursor walk handles the remaining 7 extractors.
12511251
cbm_extract_definitions(&ctx);
1252+
cbm_extract_embedded_defs(&ctx); // defs inside <cfscript> blocks of CFML tag files
12521253
cbm_extract_imports(&ctx);
12531254
cbm_extract_unified(&ctx);
12541255

‎internal/cbm/cbm.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,8 @@ void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);
726726
// --- Sub-extractor entry points ---
727727

728728
void cbm_extract_definitions(CBMExtractCtx *ctx);
729+
void cbm_extract_definitions_body(CBMExtractCtx *ctx); // no Module node (for sub-trees)
730+
void cbm_extract_embedded_defs(CBMExtractCtx *ctx); // re-parse embedded script blocks for defs
729731
void cbm_extract_imports(CBMExtractCtx *ctx);
730732
void cbm_extract_usages(CBMExtractCtx *ctx);
731733
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
@@ -7424,6 +7424,20 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
74247424
free(s.data);
74257425
}
74267426

7427+
// Walk the tree for functions/classes/fields and module-level variables, WITHOUT
7428+
// emitting the file-level Module node. Split out so embedded sub-trees (e.g. a
7429+
// CFML <cfscript> block re-parsed with the cfscript grammar) can be walked for
7430+
// definitions without minting a spurious Module per block. See
7431+
// cbm_extract_embedded_defs().
7432+
void cbm_extract_definitions_body(CBMExtractCtx *ctx) {
7433+
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
7434+
if (!spec) {
7435+
return;
7436+
}
7437+
walk_defs(ctx, ctx->root, spec, 0);
7438+
extract_variables(ctx, ctx->root, spec);
7439+
}
7440+
74277441
void cbm_extract_definitions(CBMExtractCtx *ctx) {
74287442
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
74297443
if (!spec) {
@@ -7445,9 +7459,6 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
74457459
mod.is_test = ctx->result->is_test_file;
74467460
cbm_defs_push(&ctx->result->defs, a, mod);
74477461

7448-
// Walk AST for function/class definitions
7449-
walk_defs(ctx, ctx->root, spec, 0);
7450-
7451-
// Extract module-level variables
7452-
extract_variables(ctx, ctx->root, spec);
7462+
// Walk AST for definitions + module-level variables
7463+
cbm_extract_definitions_body(ctx);
74537464
}

‎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",
@@ -861,24 +871,24 @@ static const char *graphql_field_types[] = {"field_definition", "input_value_def
861871
// so the existing ES import extractor sees real import_statement nodes.
862872
// Terminator: an entry whose script_node_type is NULL.
863873
static const CBMEmbeddedLangSpec vue_embedded_imports[] = {
864-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
865-
{NULL, NULL, 0},
874+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
875+
{NULL, NULL, 0, false},
866876
};
867877
static const CBMEmbeddedLangSpec svelte_embedded_imports[] = {
868-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
869-
{NULL, NULL, 0},
878+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
879+
{NULL, NULL, 0, false},
870880
};
871881
static const CBMEmbeddedLangSpec html_embedded_imports[] = {
872-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
873-
{NULL, NULL, 0},
882+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
883+
{NULL, NULL, 0, false},
874884
};
875885
static const CBMEmbeddedLangSpec astro_embedded_imports[] = {
876886
/* Astro component scripts live in the `---` frontmatter fence, which the
877887
* grammar keeps as an unparsed frontmatter_js_block. Re-parse that slice
878888
* with the JS grammar so `import X from './X.astro'` becomes a real edge. */
879-
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT},
880-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
881-
{NULL, NULL, 0},
889+
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT, false},
890+
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
891+
{NULL, NULL, 0, false},
882892
};
883893

884894
// ==================== VUE ====================
@@ -2064,7 +2074,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
20642074
[CBM_LANG_CFML] = {CBM_LANG_CFML, cfml_func_types, empty_types, empty_types, cfml_module_types,
20652075
cfml_call_types, empty_types, empty_types, cfml_branch_types, empty_types,
20662076
empty_types, empty_types, NULL, empty_types, NULL, NULL, tree_sitter_cfml,
2067-
NULL},
2077+
cfml_embedded_imports},
20682078

20692079
// CBM_LANG_GLEAM
20702080
[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
@@ -666,6 +666,10 @@ static CBMLanguage detect_file_language(const char *entry_name, const char *abs_
666666
if (dot && strcmp(dot, ".inc") == 0) {
667667
lang = cbm_disambiguate_inc(abs_path);
668668
}
669+
/* Special: .cfc components may be script-dialect or tag-dialect (<cfcomponent>) */
670+
if (dot && strcmp(dot, ".cfc") == 0) {
671+
lang = cbm_disambiguate_cfc(abs_path);
672+
}
669673
/* Special: ObjectScript Studio Export XML (<Export generator="...">) is
670674
* detected by content; otherwise .xml stays XML. */
671675
if (lang == CBM_LANG_XML) {

‎src/discover/discover.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ CBMLanguage cbm_disambiguate_cls(const char *path);
5050
* On read failure, defaults to CBM_LANG_BITBAKE. */
5151
CBMLanguage cbm_disambiguate_inc(const char *path);
5252

53+
/* Disambiguate .cfc files by reading the head of the content.
54+
* Returns CBM_LANG_CFML if the component is tag-based (a "<cfcomponent" tag, or a
55+
* leading '<'), otherwise CBM_LANG_CFSCRIPT for script-dialect components.
56+
* On read failure, defaults to CBM_LANG_CFSCRIPT. */
57+
CBMLanguage cbm_disambiguate_cfc(const char *path);
58+
5359
/* ── Gitignore pattern matching ──────────────────────────────────── */
5460

5561
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

@@ -1122,3 +1126,74 @@ CBMLanguage cbm_disambiguate_inc(const char *path) {
11221126
}
11231127
return CBM_LANG_BITBAKE;
11241128
}
1129+
1130+
/* Case-insensitive prefix match (portable — no strncasecmp dependency). */
1131+
static bool starts_with_ci(const char *s, const char *prefix) {
1132+
for (; *prefix; s++, prefix++) {
1133+
if (tolower((unsigned char)*s) != tolower((unsigned char)*prefix)) {
1134+
return false;
1135+
}
1136+
}
1137+
return true;
1138+
}
1139+
1140+
/* Disambiguate .cfc files: a ColdFusion component may be written in the script
1141+
* dialect ("component { ... }", parsed by the JS-like cfscript grammar) or the
1142+
* tag dialect ("<cfcomponent> ... <cffunction>", parsed by the HTML-derived cfml
1143+
* grammar). The extension table defaults to cfscript because that is what modern
1144+
* Lucee/ACF templates use, but large legacy codebases are predominantly tag-based
1145+
* and feeding those to the wrong grammar fails wholesale. Routing rules:
1146+
* 1. A "<cfcomponent" or top-level "<cffunction" tag ⇒ tag dialect. (The latter
1147+
* catches "bare" tag components that omit the <cfcomponent> wrapper.) This
1148+
* wins regardless of any leading <!---/<cfscript>, so it is checked first.
1149+
* 2. Otherwise the file is script-dialect content. Find the first significant
1150+
* token, skipping whitespace and <!--- ---> comments:
1151+
* - a leading "<cfscript>" wrapper is still script content ⇒ cfscript;
1152+
* - a different leading tag (e.g. <cfquery> in a bare-tag file) ⇒ cfml;
1153+
* - anything else ("component { ... }") ⇒ cfscript.
1154+
* Defaults to CBM_LANG_CFSCRIPT on any doubt (preserves table behaviour). */
1155+
CBMLanguage cbm_disambiguate_cfc(const char *path) {
1156+
if (!path) {
1157+
return CBM_LANG_CFSCRIPT;
1158+
}
1159+
1160+
FILE *f = cbm_fopen(path, "r");
1161+
if (!f) {
1162+
return CBM_LANG_CFSCRIPT;
1163+
}
1164+
1165+
/* Read a generous head: tag components can carry a large license/revision
1166+
* comment block before the <cfcomponent> opener. */
1167+
char buf[CBM_SZ_16K + SKIP_ONE];
1168+
size_t n = fread(buf, SKIP_ONE, CBM_SZ_16K, f);
1169+
buf[n] = '\0';
1170+
(void)fclose(f);
1171+
1172+
/* Rule 1: explicit tag-component markers ⇒ tag dialect. */
1173+
if (cbm_strcasestr(buf, "<cfcomponent") != NULL || cbm_strcasestr(buf, "<cffunction") != NULL) {
1174+
return CBM_LANG_CFML;
1175+
}
1176+
1177+
/* Rule 2: locate the first significant token, past whitespace and comments. */
1178+
const char *p = buf;
1179+
for (;;) {
1180+
while (*p && isspace((unsigned char)*p)) {
1181+
p++;
1182+
}
1183+
if (starts_with_ci(p, "<!---")) {
1184+
const char *end = strstr(p + SLEN("<!---"), "--->");
1185+
if (!end) {
1186+
break; /* comment runs past the buffer — treat as no token */
1187+
}
1188+
p = end + SLEN("--->");
1189+
continue;
1190+
}
1191+
break;
1192+
}
1193+
if (*p == '<') {
1194+
/* A leading <cfscript> wrapper is script content; any other leading tag
1195+
* (bare-tag file) is tag content. */
1196+
return starts_with_ci(p, "<cfscript") ? CBM_LANG_CFSCRIPT : CBM_LANG_CFML;
1197+
}
1198+
return CBM_LANG_CFSCRIPT;
1199+
}

0 commit comments

Comments
 (0)