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
1 change: 1 addition & 0 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
// Run extractors: defs + imports use separate walks (unique recursion patterns),
// then a single unified cursor walk handles the remaining 7 extractors.
cbm_extract_definitions(&ctx);
cbm_extract_embedded_defs(&ctx); // defs inside <cfscript> blocks of CFML tag files
cbm_extract_imports(&ctx);
cbm_extract_unified(&ctx);

Expand Down
2 changes: 2 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,8 @@ void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);
// --- Sub-extractor entry points ---

void cbm_extract_definitions(CBMExtractCtx *ctx);
void cbm_extract_definitions_body(CBMExtractCtx *ctx); // no Module node (for sub-trees)
void cbm_extract_embedded_defs(CBMExtractCtx *ctx); // re-parse embedded script blocks for defs
void cbm_extract_imports(CBMExtractCtx *ctx);
void cbm_extract_usages(CBMExtractCtx *ctx);
void cbm_extract_semantic(CBMExtractCtx *ctx);
Expand Down
21 changes: 16 additions & 5 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -7424,6 +7424,20 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
free(s.data);
}

// Walk the tree for functions/classes/fields and module-level variables, WITHOUT
// emitting the file-level Module node. Split out so embedded sub-trees (e.g. a
// CFML <cfscript> block re-parsed with the cfscript grammar) can be walked for
// definitions without minting a spurious Module per block. See
// cbm_extract_embedded_defs().
void cbm_extract_definitions_body(CBMExtractCtx *ctx) {
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
if (!spec) {
return;
}
walk_defs(ctx, ctx->root, spec, 0);
extract_variables(ctx, ctx->root, spec);
}

void cbm_extract_definitions(CBMExtractCtx *ctx) {
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
if (!spec) {
Expand All @@ -7445,9 +7459,6 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
mod.is_test = ctx->result->is_test_file;
cbm_defs_push(&ctx->result->defs, a, mod);

// Walk AST for function/class definitions
walk_defs(ctx, ctx->root, spec, 0);

// Extract module-level variables
extract_variables(ctx, ctx->root, spec);
// Walk AST for definitions + module-level variables
cbm_extract_definitions_body(ctx);
}
72 changes: 72 additions & 0 deletions internal/cbm/extract_imports.c
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,78 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
}
}

// Re-parse embedded script blocks flagged extract_definitions in embedded_imports
// and walk them for DEFINITIONS, not imports. Mirrors parse_embedded_imports but
// runs cbm_extract_definitions_body on each inner AST. Because the inner tree is
// parsed from a slice starting at the block, its node rows are block-relative;
// after extracting, we shift the newly-added definitions' line numbers by the
// block's start row so they map back to the host file. CBMDefinition carries
// only line positions (no byte offsets), so a constant row shift is a complete
// remap. Definitions only — calls/usages inside the block are intentionally not
// walked here (they would need byte remapping too and could pollute the call
// graph), so this cannot corrupt existing edges.
void cbm_extract_embedded_defs(CBMExtractCtx *ctx) {
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
if (!spec || !spec->embedded_imports) {
return;
}
for (const CBMEmbeddedLangSpec *e = spec->embedded_imports; e->script_node_type != NULL; e++) {
if (!e->extract_definitions) {
continue; /* this embedded block is walked for imports only */
}
const TSLanguage *embedded_lang = cbm_ts_language(e->embedded_language);
if (!embedded_lang) {
continue; /* embedded grammar not linked in — silently skip */
}
enum { MAX_EMBEDDED_BLOCKS = 64 };
TSNode hits[MAX_EMBEDDED_BLOCKS];
int hit_count = 0;
embedded_collect_content_nodes(ctx->root, e, hits, &hit_count, MAX_EMBEDDED_BLOCKS);
if (hit_count == 0) {
continue;
}
TSParser *parser = ts_parser_new();
if (!parser) {
continue;
}
if (!ts_parser_set_language(parser, embedded_lang)) {
ts_parser_delete(parser);
continue;
}
for (int i = 0; i < hit_count; i++) {
uint32_t s = ts_node_start_byte(hits[i]);
uint32_t end = ts_node_end_byte(hits[i]);
if (end <= s) {
continue;
}
const char *sub_src = ctx->source + s;
uint32_t sub_len = end - s;
TSTree *sub_tree = ts_parser_parse_string(parser, NULL, sub_src, sub_len);
if (!sub_tree) {
continue;
}
CBMExtractCtx sub_ctx = *ctx;
sub_ctx.source = sub_src;
sub_ctx.source_len = (int)sub_len;
sub_ctx.language = e->embedded_language;
sub_ctx.root = ts_tree_root_node(sub_tree);

int defs_before = ctx->result->defs.count;
cbm_extract_definitions_body(&sub_ctx);

/* Shift block-relative line numbers back to host-file lines. */
uint32_t row0 = ts_node_start_point(hits[i]).row;
for (int j = defs_before; j < ctx->result->defs.count; j++) {
CBMDefinition *d = &ctx->result->defs.items[j];
d->start_line += row0;
d->end_line += row0;
}
ts_tree_delete(sub_tree);
}
ts_parser_delete(parser);
}
}

// --- Namespace / package declaration capture ---
// Java/Kotlin/C#/PHP put the file's symbols inside a namespace/package whose
// name is NOT reflected in the path-based QN scheme. Capturing it lets the
Expand Down
30 changes: 20 additions & 10 deletions internal/cbm/lang_specs.c
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ static const char *cfml_branch_types[] = {
"cf_if_tag", "cf_elseif_tag", "cf_else_tag", "if_statement",
"for_statement", "while_statement", "switch_statement", NULL};
static const char *cfml_module_types[] = {"program", "component_file", NULL};
// The cfml (HTML-derived) grammar keeps the body of a <cfscript> block as an
// opaque cf_script_content token — it does NOT parse the script-dialect
// functions inside. Re-parse that slice with the cfscript grammar (extract_
// definitions = true) so those functions become real definitions. (Contrast the
// comment above: embedded <cfscript> functions only "appear as function_
// declaration" once re-parsed here; in the raw cfml tree they are unparsed text.)
static const CBMEmbeddedLangSpec cfml_embedded_imports[] = {
{"cf_script_tag", "cf_script_content", CBM_LANG_CFSCRIPT, true},
{NULL, NULL, 0, false},
};

// ==================== RUST ====================
static const char *rust_func_types[] = {"function_item", "function_signature_item",
Expand Down Expand Up @@ -861,24 +871,24 @@ static const char *graphql_field_types[] = {"field_definition", "input_value_def
// so the existing ES import extractor sees real import_statement nodes.
// Terminator: an entry whose script_node_type is NULL.
static const CBMEmbeddedLangSpec vue_embedded_imports[] = {
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
{NULL, NULL, 0},
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
{NULL, NULL, 0, false},
};
static const CBMEmbeddedLangSpec svelte_embedded_imports[] = {
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
{NULL, NULL, 0},
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
{NULL, NULL, 0, false},
};
static const CBMEmbeddedLangSpec html_embedded_imports[] = {
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
{NULL, NULL, 0},
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
{NULL, NULL, 0, false},
};
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},
{NULL, NULL, 0},
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT, false},
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT, false},
{NULL, NULL, 0, false},
};

// ==================== VUE ====================
Expand Down Expand Up @@ -2064,7 +2074,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
[CBM_LANG_CFML] = {CBM_LANG_CFML, cfml_func_types, empty_types, empty_types, cfml_module_types,
cfml_call_types, empty_types, empty_types, cfml_branch_types, empty_types,
empty_types, empty_types, NULL, empty_types, NULL, NULL, tree_sitter_cfml,
NULL},
cfml_embedded_imports},

// CBM_LANG_GLEAM
[CBM_LANG_GLEAM] = {CBM_LANG_GLEAM, gleam_func_types, gleam_class_types, gleam_field_types,
Expand Down
7 changes: 7 additions & 0 deletions internal/cbm/lang_specs.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ typedef struct {
const char *script_node_type; // e.g. "script_element"
const char *content_node_type; // e.g. "raw_text"
CBMLanguage embedded_language; // grammar used to re-parse the content slice
// When true, the re-parsed inner AST is also walked for DEFINITIONS, not just
// imports — e.g. CFML tag components whose <cfscript> body (cf_script_tag ->
// cf_script_content) holds script-dialect functions the HTML-derived cfml
// grammar keeps as opaque content. Kept on this small struct (a handful of
// instances) rather than as a CBMLangSpec field, which would trip
// -Wmissing-field-initializers across every language row.
bool extract_definitions;
} CBMEmbeddedLangSpec;

// CBMLangSpec mirrors Go's lang.LanguageSpec with NULL-terminated string arrays.
Expand Down
4 changes: 4 additions & 0 deletions src/discover/discover.c
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,10 @@ static CBMLanguage detect_file_language(const char *entry_name, const char *abs_
if (dot && strcmp(dot, ".inc") == 0) {
lang = cbm_disambiguate_inc(abs_path);
}
/* Special: .cfc components may be script-dialect or tag-dialect (<cfcomponent>) */
if (dot && strcmp(dot, ".cfc") == 0) {
lang = cbm_disambiguate_cfc(abs_path);
}
/* Special: ObjectScript Studio Export XML (<Export generator="...">) is
* detected by content; otherwise .xml stays XML. */
if (lang == CBM_LANG_XML) {
Expand Down
6 changes: 6 additions & 0 deletions src/discover/discover.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ CBMLanguage cbm_disambiguate_cls(const char *path);
* On read failure, defaults to CBM_LANG_BITBAKE. */
CBMLanguage cbm_disambiguate_inc(const char *path);

/* Disambiguate .cfc files by reading the head of the content.
* Returns CBM_LANG_CFML if the component is tag-based (a "<cfcomponent" tag, or a
* leading '<'), otherwise CBM_LANG_CFSCRIPT for script-dialect components.
* On read failure, defaults to CBM_LANG_CFSCRIPT. */
CBMLanguage cbm_disambiguate_cfc(const char *path);

/* ── Gitignore pattern matching ──────────────────────────────────── */

typedef struct cbm_gitignore cbm_gitignore_t;
Expand Down
77 changes: 76 additions & 1 deletion src/discover/language.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "cbm.h" // CBMLanguage, CBM_LANG_*

#include "foundation/constants.h"
#include "foundation/compat.h" // cbm_strcasestr
#include "foundation/compat_fs.h"

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

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

Expand Down Expand Up @@ -1122,3 +1126,74 @@ CBMLanguage cbm_disambiguate_inc(const char *path) {
}
return CBM_LANG_BITBAKE;
}

/* Case-insensitive prefix match (portable — no strncasecmp dependency). */
static bool starts_with_ci(const char *s, const char *prefix) {
for (; *prefix; s++, prefix++) {
if (tolower((unsigned char)*s) != tolower((unsigned char)*prefix)) {
return false;
}
}
return true;
}

/* Disambiguate .cfc files: a ColdFusion component may be written in the script
* dialect ("component { ... }", parsed by the JS-like cfscript grammar) or the
* tag dialect ("<cfcomponent> ... <cffunction>", parsed by the HTML-derived cfml
* grammar). The extension table defaults to cfscript because that is what modern
* Lucee/ACF templates use, but large legacy codebases are predominantly tag-based
* and feeding those to the wrong grammar fails wholesale. Routing rules:
* 1. A "<cfcomponent" or top-level "<cffunction" tag ⇒ tag dialect. (The latter
* catches "bare" tag components that omit the <cfcomponent> wrapper.) This
* wins regardless of any leading <!---/<cfscript>, so it is checked first.
* 2. Otherwise the file is script-dialect content. Find the first significant
* token, skipping whitespace and <!--- ---> comments:
* - a leading "<cfscript>" wrapper is still script content ⇒ cfscript;
* - a different leading tag (e.g. <cfquery> in a bare-tag file) ⇒ cfml;
* - anything else ("component { ... }") ⇒ cfscript.
* Defaults to CBM_LANG_CFSCRIPT on any doubt (preserves table behaviour). */
CBMLanguage cbm_disambiguate_cfc(const char *path) {
if (!path) {
return CBM_LANG_CFSCRIPT;
}

FILE *f = cbm_fopen(path, "r");
if (!f) {
return CBM_LANG_CFSCRIPT;
}

/* Read a generous head: tag components can carry a large license/revision
* comment block before the <cfcomponent> opener. */
char buf[CBM_SZ_16K + SKIP_ONE];
size_t n = fread(buf, SKIP_ONE, CBM_SZ_16K, f);
buf[n] = '\0';
(void)fclose(f);

/* Rule 1: explicit tag-component markers ⇒ tag dialect. */
if (cbm_strcasestr(buf, "<cfcomponent") != NULL || cbm_strcasestr(buf, "<cffunction") != NULL) {
return CBM_LANG_CFML;
}

/* Rule 2: locate the first significant token, past whitespace and comments. */
const char *p = buf;
for (;;) {
while (*p && isspace((unsigned char)*p)) {
p++;
}
if (starts_with_ci(p, "<!---")) {
const char *end = strstr(p + SLEN("<!---"), "--->");
if (!end) {
break; /* comment runs past the buffer — treat as no token */
}
p = end + SLEN("--->");
continue;
}
break;
}
if (*p == '<') {
/* A leading <cfscript> wrapper is script content; any other leading tag
* (bare-tag file) is tag content. */
return starts_with_ci(p, "<cfscript") ? CBM_LANG_CFSCRIPT : CBM_LANG_CFML;
}
return CBM_LANG_CFSCRIPT;
}
Loading
Loading