diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 5d6309c7e..858f59db0 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -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 blocks of CFML tag files cbm_extract_imports(&ctx); cbm_extract_unified(&ctx); diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 440d80fce..a8fbecf15 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -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); diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 3270bacc3..17df825dd 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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 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) { @@ -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); } diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index 8696e4b30..3ba252fed 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -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 diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 2bc1fe1e7..c864b647a 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -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 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 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", @@ -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 ==================== @@ -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, diff --git a/internal/cbm/lang_specs.h b/internal/cbm/lang_specs.h index ab823557b..e37d7d2e0 100644 --- a/internal/cbm/lang_specs.h +++ b/internal/cbm/lang_specs.h @@ -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 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. diff --git a/src/discover/discover.c b/src/discover/discover.c index 15243d3b6..f6aa943af 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -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 () */ + if (dot && strcmp(dot, ".cfc") == 0) { + lang = cbm_disambiguate_cfc(abs_path); + } /* Special: ObjectScript Studio Export XML () is * detected by content; otherwise .xml stays XML. */ if (lang == CBM_LANG_XML) { diff --git a/src/discover/discover.h b/src/discover/discover.h index 87fc159d7..f7a73a835 100644 --- a/src/discover/discover.h +++ b/src/discover/discover.h @@ -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 " ...). The + * table default is script; tag-based .cfc are resolved by content in + * cbm_disambiguate_cfc(). */ {".cfc", CBM_LANG_CFSCRIPT}, {".cfm", CBM_LANG_CFML}, @@ -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 (" ... ", 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 " wrapper.) This + * wins regardless of any leading comments: + * - a leading "" wrapper is still script content ⇒ cfscript; + * - a different leading tag (e.g. 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 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, ""); + if (!end) { + break; /* comment runs past the buffer — treat as no token */ + } + p = end + SLEN("--->"); + continue; + } + break; + } + if (*p == '<') { + /* A leading wrapper is script content; any other leading tag + * (bare-tag file) is tag content. */ + return starts_with_ci(p, " block of a tag + * component. The HTML-derived cfml grammar keeps the body as an + * opaque cf_script_content token, so these functions only surface once the + * block is re-parsed with the cfscript grammar (cbm_extract_embedded_defs). + * Also asserts the block-relative line numbers are remapped back to host-file + * lines, and that a leading 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 , 2 , 3 , + * 4 greet(), 7 addTwo(), 10 , 11 , 14 close. */ + CBMFileResult *r = extract("\n" + "\n" + "\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" + "\n" + "\n" + " \n" + "\n" + "\n", + CBM_LANG_CFML, "app", "Service.cfc"); + ASSERT_NOT_NULL(r); + /* Script functions inside are recovered ... */ + ASSERT(has_def(r, "Function", "greet")); + ASSERT(has_def(r, "Function", "addTwo")); + /* ... alongside the tag-dialect 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" @@ -1581,14 +1626,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")); @@ -1600,10 +1644,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")); @@ -3183,9 +3226,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(); } @@ -3230,7 +3273,6 @@ TEST(extract_ts_template_string_url_issue1006) { 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 @@ -5225,6 +5267,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); diff --git a/tests/test_language.c b/tests/test_language.c index e84bbc1b8..f3a2e4b2b 100644 --- a/tests/test_language.c +++ b/tests/test_language.c @@ -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) { + /* wrapper ⇒ tag dialect. */ + ASSERT_EQ(disambiguate_cfc_content("test_cfc_tag.cfc", + "\n\n" + "\n"), + CBM_LANG_CFML); + PASS(); +} + +TEST(lang_cfc_bare_cffunction) { + /* A component that omits but uses is still tag. */ + ASSERT_EQ(disambiguate_cfc_content("test_cfc_bare.cfc", + "\n" + "\n\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 is still script — the + * leading '<' must NOT route it to the tag grammar. */ + ASSERT_EQ( + disambiguate_cfc_content("test_cfc_wrapped.cfc", + "\ncomponent {\n function f() { return 1; }\n}\n" + "\n"), + CBM_LANG_CFSCRIPT); + PASS(); +} + +TEST(lang_cfc_tag_after_license_comment) { + /* A leading license comment before ⇒ tag. */ + ASSERT_EQ(disambiguate_cfc_content("test_cfc_licensed.cfc", + "\n\n\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", + " \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); @@ -1209,6 +1295,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 */