Skip to content
Merged
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
17 changes: 15 additions & 2 deletions internal/cbm/extract_imports.c
Original file line number Diff line number Diff line change
Expand Up @@ -1588,8 +1588,18 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
embedded_collect_content_nodes(ctx->root, e, hits, &hit_count, MAX_EMBEDDED_BLOCKS);
for (int i = 0; i < hit_count; i++) {
CBMLanguage embedded_language = e->embedded_language;
bool extract_structure = ctx->language == CBM_LANG_VUE;
if (extract_structure &&
/* Structure (defs + calls), not just imports, for hosts whose
* embedded language carries real code. Vue since #1852; CFML's
* <cfscript> since the #1412 distillation. */
bool extract_structure =
ctx->language == CBM_LANG_VUE || ctx->language == CBM_LANG_CFML;
/* The attribute resolver is Vue's: it inspects <script lang=/src=>
* and OVERRIDES the spec's embedded language (JS default, TS on
* lang="ts", bail on src=). CFML's cf_script_tag carries no such
* attributes and its embedded language is fixed by the spec row
* (CFSCRIPT) — running the resolver would silently rewrite it to
* JavaScript. Resolve only for Vue. */
if (ctx->language == CBM_LANG_VUE &&
!vue_embedded_language(ctx, hits[i].script, &embedded_language)) {
continue;
}
Expand Down Expand Up @@ -3083,6 +3093,9 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
case CBM_LANG_SVELTE:
case CBM_LANG_VUE:
case CBM_LANG_ASTRO:
/* Tag-dialect CFML: <cfscript> bodies are opaque cf_script_content to the
* cfml grammar; the embedded spec re-parses them as CFSCRIPT (#1412). */
case CBM_LANG_CFML:
parse_embedded_imports(ctx);
break;
default:
Expand Down
10 changes: 9 additions & 1 deletion internal/cbm/lang_specs.c
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,14 @@ static const char *graphql_field_types[] = {"field_definition", "input_value_def
// the generic embedded-imports walker re-parse that slice with the JS grammar
// so the existing ES import extractor sees real import_statement nodes.
// Terminator: an entry whose script_node_type is NULL.
static const CBMEmbeddedLangSpec cfml_embedded_imports[] = {
/* Tag-dialect CFML keeps <cfscript> bodies as opaque cf_script_content;
* re-parse them with the cfscript grammar through the shared included-
* ranges machinery so defs (and calls) inside legacy components extract
* with absolute coordinates. Distilled from #1412. */
{"cf_script_tag", "cf_script_content", CBM_LANG_CFSCRIPT},
{NULL, NULL, 0},
};
static const CBMEmbeddedLangSpec vue_embedded_imports[] = {
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
{NULL, NULL, 0},
Expand Down Expand Up @@ -2119,7 +2127,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
4 changes: 4 additions & 0 deletions src/discover/discover.c
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,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 @@ -64,6 +64,12 @@ CBMLanguage cbm_disambiguate_inc(const char *path);
* shebang, an embedded NUL in the first line, or an unknown interpreter. */
CBMLanguage cbm_language_from_shebang(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 @@ -469,7 +470,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 @@ -1306,3 +1310,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;
}
76 changes: 60 additions & 16 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,52 @@ TEST(extract_cfml_tag_issue38) {
PASS();
}

/* --- CFML tag dialect: script functions inside a <cfscript> block of a tag
* component. The HTML-derived cfml grammar keeps the <cfscript> body as an
* opaque cf_script_content token, so these functions only surface once the
* block is re-parsed with the cfscript grammar through the shared
* included-ranges machinery (parse_one_embedded_block, #1852).
* Also asserts the block-relative line numbers are remapped back to host-file
* lines, and that a leading <cfsetting> void tag (which cascades ERROR nodes in
* the cfml grammar) does not prevent recovery of the functions. --- */
TEST(extract_cfml_embedded_cfscript_defs) {
/* Source layout (1-based lines): 1 <cfcomponent>, 2 <cfsetting>, 3 <cfscript>,
* 4 greet(), 7 addTwo(), 10 </cfscript>, 11 <cffunction tagPing>, 14 close. */
CBMFileResult *r = extract("<cfcomponent>\n"
"<cfsetting requesttimeout=\"10\">\n"
"<cfscript>\n"
" public string function greet(string who) {\n"
" return \"hi \" & who;\n"
" }\n"
" private numeric function addTwo(numeric a) {\n"
" return a + 2;\n"
" }\n"
"</cfscript>\n"
"<cffunction name=\"tagPing\" returntype=\"string\">\n"
" <cfreturn \"pong\">\n"
"</cffunction>\n"
"</cfcomponent>\n",
CBM_LANG_CFML, "app", "Service.cfc");
ASSERT_NOT_NULL(r);
/* Script functions inside <cfscript> are recovered ... */
ASSERT(has_def(r, "Function", "greet"));
ASSERT(has_def(r, "Function", "addTwo"));
/* ... alongside the tag-dialect <cffunction> in the same component. */
ASSERT(has_def(r, "Function", "tagPing"));
/* Line numbers are remapped from block-relative back to host-file lines. */
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (strcmp(d->label, "Function") == 0 && strcmp(d->name, "greet") == 0) {
ASSERT(d->start_line == 4);
}
if (strcmp(d->label, "Function") == 0 && strcmp(d->name, "addTwo") == 0) {
ASSERT(d->start_line == 7);
}
}
cbm_free_result(r);
PASS();
}

/* --- Helm / Go template: named templates + include calls (#338) --- */
TEST(extract_helm_templates_issue338) {
CBMFileResult *r = extract("{{- define \"chart.fullname\" -}}\n"
Expand Down Expand Up @@ -1689,14 +1735,13 @@ TEST(cpp_function) {
* node when multiple tests share a file. Each must mint a distinct Function
* node whose name encodes the suite and case arguments. */
TEST(cpp_gtest_same_name_collision_issue1266) {
CBMFileResult *r = extract(
"namespace demo { int assembleWidget(int s) { return s * 2; } }\n"
"TEST(WidgetSuite, DoublesSmallSize) { demo::assembleWidget(1); }\n"
"TEST(WidgetSuite, DoublesZero) { demo::assembleWidget(0); }\n"
"TEST(WidgetSuite, DoublesLargeSize) {\n"
" demo::assembleWidget(1000);\n"
"}\n",
CBM_LANG_CPP, "t", "direct_test.cpp");
CBMFileResult *r = extract("namespace demo { int assembleWidget(int s) { return s * 2; } }\n"
"TEST(WidgetSuite, DoublesSmallSize) { demo::assembleWidget(1); }\n"
"TEST(WidgetSuite, DoublesZero) { demo::assembleWidget(0); }\n"
"TEST(WidgetSuite, DoublesLargeSize) {\n"
" demo::assembleWidget(1000);\n"
"}\n",
CBM_LANG_CPP, "t", "direct_test.cpp");
ASSERT_NOT_NULL(r);
ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesSmallSize"));
ASSERT(has_def(r, "Function", "TEST_WidgetSuite_DoublesZero"));
Expand All @@ -1708,10 +1753,9 @@ TEST(cpp_gtest_same_name_collision_issue1266) {

/* #1266: TEST_F fixture macro also produces unique names. */
TEST(cpp_gtest_f_unique_name_issue1266) {
CBMFileResult *r = extract(
"TEST_F(MyFixture, FirstTest) { doStuff(); }\n"
"TEST_F(MyFixture, SecondTest) { doOtherStuff(); }\n",
CBM_LANG_CPP, "t", "fixture_test.cpp");
CBMFileResult *r = extract("TEST_F(MyFixture, FirstTest) { doStuff(); }\n"
"TEST_F(MyFixture, SecondTest) { doOtherStuff(); }\n",
CBM_LANG_CPP, "t", "fixture_test.cpp");
ASSERT_NOT_NULL(r);
ASSERT(has_def(r, "Function", "TEST_F_MyFixture_FirstTest"));
ASSERT(has_def(r, "Function", "TEST_F_MyFixture_SecondTest"));
Expand Down Expand Up @@ -3676,9 +3720,9 @@ TEST(extract_ts_decorators_survive_interleaved_comment) {
ASSERT_FALSE(r->has_error);
const CBMDefinition *m = find_def_by_name(r, "login");
ASSERT_NOT_NULL(m);
ASSERT(decorators_contain(m, "Throttle")); /* below the comment — always worked */
ASSERT(decorators_contain(m, "HttpCode")); /* above the comment — was dropped */
ASSERT(decorators_contain(m, "Post")); /* above the comment — was dropped */
ASSERT(decorators_contain(m, "Throttle")); /* below the comment — always worked */
ASSERT(decorators_contain(m, "HttpCode")); /* above the comment — was dropped */
ASSERT(decorators_contain(m, "Post")); /* above the comment — was dropped */
cbm_free_result(r);
PASS();
}
Expand Down Expand Up @@ -3937,7 +3981,6 @@ TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249) {
PASS();
}


/* Reproduce-first: Java module QN must derive from the CONTAINING DIRECTORY, not
* the filename stem, so a top-level class `Outer` in `Outer.java` is `t.Outer`,
* NOT the doubled `t.Outer.Outer`. The nested method def QN must also equal the
Expand Down Expand Up @@ -6037,6 +6080,7 @@ SUITE(extraction) {
RUN_TEST(extract_qml_issue42);
RUN_TEST(extract_cfscript_issue38);
RUN_TEST(extract_cfml_tag_issue38);
RUN_TEST(extract_cfml_embedded_cfscript_defs);
RUN_TEST(extract_helm_templates_issue338);
RUN_TEST(extract_helm_values_toplevel_issue338);

Expand Down
94 changes: 94 additions & 0 deletions tests/test_language.c
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,92 @@ TEST(lang_m_default_on_read_fail) {
PASS();
}

/* ── .cfc disambiguation (tag vs script dialect) ───────────────── */

/* Helper: write content to a temp .cfc and return its disambiguated language. */
static CBMLanguage disambiguate_cfc_content(const char *name, const char *content) {
char path[256];
snprintf(path, sizeof(path), "%s/%s", cbm_tmpdir(), name);
FILE *f = fopen(path, "w");
if (!f) {
return CBM_LANG_COUNT;
}
fputs(content, f);
fclose(f);
CBMLanguage lang = cbm_disambiguate_cfc(path);
remove(path);
return lang;
}

TEST(lang_cfc_tag_component) {
/* <cfcomponent> wrapper ⇒ tag dialect. */
ASSERT_EQ(disambiguate_cfc_content("test_cfc_tag.cfc",
"<cfcomponent>\n<cffunction name=\"f\"></cffunction>\n"
"</cfcomponent>\n"),
CBM_LANG_CFML);
PASS();
}

TEST(lang_cfc_bare_cffunction) {
/* A component that omits <cfcomponent> but uses <cffunction> is still tag. */
ASSERT_EQ(disambiguate_cfc_content("test_cfc_bare.cfc",
"<cffunction name=\"f\" output=\"false\">\n"
"<cfreturn 1>\n</cffunction>\n"),
CBM_LANG_CFML);
PASS();
}

TEST(lang_cfc_script_component) {
/* Plain "component { ... }" ⇒ script dialect. */
ASSERT_EQ(disambiguate_cfc_content("test_cfc_script.cfc",
"component {\n function f() { return 1; }\n}\n"),
CBM_LANG_CFSCRIPT);
PASS();
}

TEST(lang_cfc_script_bare_keyword) {
/* "component" on its own line (brace on the next) ⇒ script dialect. */
ASSERT_EQ(disambiguate_cfc_content("test_cfc_kw.cfc",
"component\n{\n function f() { return 1; }\n}\n"),
CBM_LANG_CFSCRIPT);
PASS();
}

TEST(lang_cfc_cfscript_wrapped_script) {
/* A script component wrapped in a leading <cfscript> is still script — the
* leading '<' must NOT route it to the tag grammar. */
ASSERT_EQ(
disambiguate_cfc_content("test_cfc_wrapped.cfc",
"<cfscript>\ncomponent {\n function f() { return 1; }\n}\n"
"</cfscript>\n"),
CBM_LANG_CFSCRIPT);
PASS();
}

TEST(lang_cfc_tag_after_license_comment) {
/* A leading <!--- ---> license comment before <cfcomponent> ⇒ tag. */
ASSERT_EQ(disambiguate_cfc_content("test_cfc_licensed.cfc",
"<!---\n Copyright\n--->\n<cfcomponent>\n</cfcomponent>\n"),
CBM_LANG_CFML);
PASS();
}

TEST(lang_cfc_script_after_license_comment) {
/* A leading <!--- ---> comment before a script component ⇒ script (the
* comment's '<' must be skipped, not treated as a tag opener). */
ASSERT_EQ(disambiguate_cfc_content("test_cfc_lic_script.cfc",
"<!--- header ---> \ncomponent {\n"
" function f() { return 1; }\n}\n"),
CBM_LANG_CFSCRIPT);
PASS();
}

TEST(lang_cfc_default_on_read_fail) {
/* Non-existent file defaults to script dialect. */
ASSERT_EQ(cbm_disambiguate_cfc("/tmp/nonexistent_file_98765.cfc"), CBM_LANG_CFSCRIPT);
PASS();
}

/* --- New languages (auto-generated) --- */
TEST(lang_ext_solidity) {
ASSERT_EQ(cbm_language_for_extension(".sol"), CBM_LANG_SOLIDITY);
Expand Down Expand Up @@ -1232,6 +1318,14 @@ SUITE(language) {
RUN_TEST(lang_m_magma);
RUN_TEST(lang_m_matlab);
RUN_TEST(lang_m_default_on_read_fail);
RUN_TEST(lang_cfc_tag_component);
RUN_TEST(lang_cfc_bare_cffunction);
RUN_TEST(lang_cfc_script_component);
RUN_TEST(lang_cfc_script_bare_keyword);
RUN_TEST(lang_cfc_cfscript_wrapped_script);
RUN_TEST(lang_cfc_tag_after_license_comment);
RUN_TEST(lang_cfc_script_after_license_comment);
RUN_TEST(lang_cfc_default_on_read_fail);

/* Go test ports */
/* New languages */
Expand Down
Loading