Skip to content

Commit 0ff692d

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. Adds an embedded_defs field to CBMLangSpec, wired only to CBM_LANG_CFML. 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 d6be58e commit 0ff692d

11 files changed

Lines changed: 344 additions & 23 deletions

File tree

‎internal/cbm/cbm.c‎

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

‎internal/cbm/cbm.h‎

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

657657
void cbm_extract_definitions(CBMExtractCtx *ctx);
658+
void cbm_extract_definitions_body(CBMExtractCtx *ctx); // no Module node (for sub-trees)
659+
void cbm_extract_embedded_defs(CBMExtractCtx *ctx); // re-parse embedded script blocks for defs
658660
void cbm_extract_imports(CBMExtractCtx *ctx);
659661
void cbm_extract_usages(CBMExtractCtx *ctx);
660662
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
@@ -7050,6 +7050,20 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
70507050
free(s.data);
70517051
}
70527052

7053+
// Walk the tree for functions/classes/fields and module-level variables, WITHOUT
7054+
// emitting the file-level Module node. Split out so embedded sub-trees (e.g. a
7055+
// CFML <cfscript> block re-parsed with the cfscript grammar) can be walked for
7056+
// definitions without minting a spurious Module per block. See
7057+
// cbm_extract_embedded_defs().
7058+
void cbm_extract_definitions_body(CBMExtractCtx *ctx) {
7059+
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
7060+
if (!spec) {
7061+
return;
7062+
}
7063+
walk_defs(ctx, ctx->root, spec, 0);
7064+
extract_variables(ctx, ctx->root, spec);
7065+
}
7066+
70537067
void cbm_extract_definitions(CBMExtractCtx *ctx) {
70547068
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
70557069
if (!spec) {
@@ -7071,9 +7085,6 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
70717085
mod.is_test = ctx->result->is_test_file;
70727086
cbm_defs_push(&ctx->result->defs, a, mod);
70737087

7074-
// Walk AST for function/class definitions
7075-
walk_defs(ctx, ctx->root, spec, 0);
7076-
7077-
// Extract module-level variables
7078-
extract_variables(ctx, ctx->root, spec);
7088+
// Walk AST for definitions + module-level variables
7089+
cbm_extract_definitions_body(ctx);
70797090
}

‎internal/cbm/extract_imports.c‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,75 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
14651465
}
14661466
}
14671467

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

‎internal/cbm/lang_specs.c‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,16 @@ static const char *cfml_branch_types[] = {
291291
"cf_if_tag", "cf_elseif_tag", "cf_else_tag", "if_statement",
292292
"for_statement", "while_statement", "switch_statement", NULL};
293293
static const char *cfml_module_types[] = {"program", "component_file", NULL};
294+
// The cfml (HTML-derived) grammar keeps the body of a <cfscript> block as an
295+
// opaque cf_script_content token — it does NOT parse the script-dialect
296+
// functions inside. Re-parse that slice with the cfscript grammar so those
297+
// functions become real definitions. (Contrast the comment above: embedded
298+
// <cfscript> functions only "appear as function_declaration" once re-parsed
299+
// here; in the raw cfml tree they are unparsed text.)
300+
static const CBMEmbeddedLangSpec cfml_embedded_defs[] = {
301+
{"cf_script_tag", "cf_script_content", CBM_LANG_CFSCRIPT},
302+
{NULL, NULL, 0},
303+
};
294304

295305
// ==================== RUST ====================
296306
static const char *rust_func_types[] = {"function_item", "function_signature_item",
@@ -2073,7 +2083,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
20732083
[CBM_LANG_CFML] = {CBM_LANG_CFML, cfml_func_types, empty_types, empty_types, cfml_module_types,
20742084
cfml_call_types, empty_types, empty_types, cfml_branch_types, empty_types,
20752085
empty_types, empty_types, NULL, empty_types, NULL, NULL, tree_sitter_cfml,
2076-
NULL},
2086+
NULL, cfml_embedded_defs},
20772087

20782088
// CBM_LANG_GLEAM
20792089
[CBM_LANG_GLEAM] = {CBM_LANG_GLEAM, gleam_func_types, gleam_class_types, gleam_field_types,

‎internal/cbm/lang_specs.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ typedef struct {
3939
// no embedded content to re-parse). The terminator is an entry whose
4040
// script_node_type is NULL.
4141
const CBMEmbeddedLangSpec *embedded_imports;
42+
// Like embedded_imports, but the inner AST is walked for DEFINITIONS (not
43+
// imports). Used where a host grammar leaves a script block unparsed yet the
44+
// block defines functions — e.g. CFML tag components whose <cfscript> body
45+
// (cf_script_tag -> cf_script_content) holds script-dialect functions the
46+
// HTML-derived cfml grammar keeps as opaque content. NULL if none.
47+
const CBMEmbeddedLangSpec *embedded_defs;
4248
} CBMLangSpec;
4349

4450
// Returns a NULL-terminated list of callee-name suffixes that indicate a

‎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)