From fd1b88374c0c21892c991c29909d22ed499c20fa Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Mon, 28 Sep 2026 08:59:32 -0500 Subject: [PATCH 1/5] fix(discover): keep make/cargo dep-info .d files out of the D grammar .d mapped to D unconditionally, so make-style dependency files written by cargo, rustc, gcc/clang -MD and CMake were parsed with the D grammar and walked at superlinear cost. Add cbm_disambiguate_d, a 4 KB first-line sniff in the shape of the existing .frm/.cls/.m disambiguators, that returns CBM_LANG_COUNT only for a make rule whose targets look like paths and keeps D on any doubt. A 200-file cargo dep-info corpus goes from 108.9 s to 1.27 s to index; 0 of 12,062 real D files are misclassified. Signed-off-by: Nelson Spence --- src/discover/discover.c | 5 +++ src/discover/discover.h | 6 +++ src/discover/language.c | 81 +++++++++++++++++++++++++++++++++++++++++ tests/test_discover.c | 48 ++++++++++++++++++++++++ tests/test_language.c | 51 ++++++++++++++++++++++++++ 5 files changed, 191 insertions(+) diff --git a/src/discover/discover.c b/src/discover/discover.c index 0e5ae5e4f9..f3518abb8e 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -803,6 +803,11 @@ static CBMLanguage detect_file_language(const char *entry_name, const char *abs_ if (dot && strcmp(dot, ".res") == 0) { lang = cbm_disambiguate_res(abs_path); } + /* Special: .d is shared by D and make-style dep-info (cargo, gcc -MD). + * Probe only while .d maps to D, so a user override to another language wins. */ + if (dot && strcmp(dot, ".d") == 0 && lang == CBM_LANG_DLANG) { + lang = cbm_disambiguate_d(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 89ccca4f36..24a08d5f44 100644 --- a/src/discover/discover.h +++ b/src/discover/discover.h @@ -57,6 +57,12 @@ CBMLanguage cbm_disambiguate_frm(const char *path); * CBM_LANG_RESCRIPT. On read failure, defaults to CBM_LANG_RESCRIPT. */ CBMLanguage cbm_disambiguate_res(const char *path); +/* Disambiguate .d files by reading first 4KB of content. + * Returns CBM_LANG_COUNT (not source) for a make-style dependency file written + * by rustc/cargo, gcc/clang -MD or CMake ("target ...: prereq ..."), otherwise + * CBM_LANG_DLANG. On read failure or any doubt, defaults to CBM_LANG_DLANG. */ +CBMLanguage cbm_disambiguate_d(const char *path); + /* Disambiguate .inc files by reading first 4KB of content. * Returns CBM_LANG_OBJECTSCRIPT_ROUTINE if it looks like an ObjectScript * include (a "ROUTINE " header), otherwise CBM_LANG_BITBAKE. diff --git a/src/discover/language.c b/src/discover/language.c index dcc41dafd0..66de4f4df9 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -1330,6 +1330,87 @@ CBMLanguage cbm_disambiguate_res(const char *path) { return memchr(buf, '\0', n) ? CBM_LANG_COUNT : CBM_LANG_RESCRIPT; } +/* A make rule's targets end at a ':' followed by whitespace or the end of the + * file, which skips drive-letter colons ("C:/", "C:\"). */ +static bool is_rule_colon(const char *p, bool whole_file) { + char next = p[SKIP_ONE]; + return *p == ':' && (next == ' ' || next == '\t' || next == '\r' || next == '\n' || + (next == '\0' && whole_file)); +} + +/* True if the line at p is a make rule "target...: prereq..." whose targets all + * look like paths (contain '/', '\' or '.'); a backslash-escaped space stays + * inside its target. D source fails this: "public:", "@safe:", "extern(C):" + * and "import a.b : c" all have a non-path word or D punctuation before the + * colon. */ +static bool is_dep_rule_line(const char *p, bool whole_file) { + bool in_target = false; + bool path_like = false; + bool any_target = false; + for (; *p && *p != '\n'; p++) { + if (is_rule_colon(p, whole_file)) { + return in_target ? path_like : any_target; + } + if (*p == ' ' || *p == '\t' || *p == '\r') { + if (in_target && !path_like) { + return false; + } + in_target = false; + path_like = false; + continue; + } + if (strchr("(){};=\"',", *p)) { + return false; + } + in_target = true; + any_target = true; + path_like = path_like || *p == '/' || *p == '\\' || *p == '.'; + if (*p == '\\' && p[SKIP_ONE] == ' ') { + p++; /* escaped space: part of this target */ + } + } + return false; +} + +/* Disambiguate .d files: shared by D source and make-style dependency files + * written by rustc/cargo (target//deps/-.d), gcc/clang -MD + * and CMake (.o.d). Those are "target: prereq ..." rules, often tens of + * KB on one line, which the D grammar parses slowly into nothing but a module + * node, so they are reported as unsupported (CBM_LANG_COUNT). Defaults to D on + * any doubt (preserves existing behaviour). */ +CBMLanguage cbm_disambiguate_d(const char *path) { + if (!path) { + return CBM_LANG_DLANG; + } + + FILE *f = cbm_fopen(path, "r"); + if (!f) { + return CBM_LANG_DLANG; + } + + char buf[CBM_SZ_4K + SKIP_ONE]; + size_t n = fread(buf, SKIP_ONE, CBM_SZ_4K, f); + buf[n] = '\0'; + (void)fclose(f); + /* A colon at the end of buf only ends the rule if nothing was cut off. */ + bool whole_file = n < CBM_SZ_4K && strlen(buf) == n; + + const char *p = buf; + if (strncmp(p, "\xEF\xBB\xBF", SLEN("\xEF\xBB\xBF")) == 0) { + p += SLEN("\xEF\xBB\xBF"); + } + while (*p && isspace((unsigned char)*p)) { + p++; + } + /* A shebang or a leading D comment (including the DUB single-file + * "/+ dub.sdl:" recipe) is D; dep-info never starts that way. */ + if (*p == '#' || + (p[0] == '/' && (p[SKIP_ONE] == '/' || p[SKIP_ONE] == '*' || p[SKIP_ONE] == '+'))) { + return CBM_LANG_DLANG; + } + return is_dep_rule_line(p, whole_file) ? CBM_LANG_COUNT : CBM_LANG_DLANG; +} + /* Disambiguate .cls files: shared by InterSystems ObjectScript UDL, Salesforce * Apex and Visual Basic 6 class modules (#721). ObjectScript class files begin * with a line of the form "Class ..."; VB6 class modules diff --git a/tests/test_discover.c b/tests/test_discover.c index 5e9bdd8af1..033eb10c97 100644 --- a/tests/test_discover.c +++ b/tests/test_discover.c @@ -6,6 +6,7 @@ #include "test_framework.h" #include "test_helpers.h" #include "discover/discover.h" +#include "discover/userconfig.h" #include "foundation/platform.h" typedef struct { @@ -1990,6 +1991,49 @@ TEST(shebang_oversized_first_line_unindexed) { PASS(); } +/* ── .d: D source vs make/cargo dep-info ───────────────────────── */ + +/* rustc/cargo dep-info (and gcc -MD, CMake .o.d) files share .d with D source + * but are Makefile rules the D grammar parses slowly into nothing useful, so + * they must not be indexed as D. */ +static const char CARGO_DEP_INFO[] = "/home/u/proj/t/debug/deps/foo-abc123.d: src/lib.rs src/a.rs\n" + "\n" + "src/lib.rs:\n" + "src/a.rs:\n"; + +TEST(discover_d_dep_info_not_indexed_as_dlang) { + CBMLanguage dep; + CBMLanguage src; + ASSERT(shebang_probe("cbm_disc_dotd", "t/debug/deps/foo-abc123.d", CARGO_DEP_INFO, &dep)); + ASSERT(shebang_probe("cbm_disc_dotd", "app.d", "module app;\nimport std.stdio : writeln;\n", + &src)); + ASSERT_EQ(dep, CBM_LANG_COUNT); + ASSERT_EQ(src, CBM_LANG_DLANG); + PASS(); +} + +/* The probe runs only while .d maps to D, so a user override to another + * language wins. */ +TEST(discover_d_user_override_skips_dep_info_probe) { + char *cfg_dir = th_mktempdir("cbm_disc_dotd_uc"); + ASSERT(cfg_dir != NULL); + ASSERT_EQ(th_write_file(TH_PATH(cfg_dir, ".codebase-memory.json"), + "{\"extra_extensions\":{\".d\":\"makefile\"}}"), + 0); + cbm_userconfig_t *cfg = cbm_userconfig_load(cfg_dir); + th_cleanup(cfg_dir); + ASSERT_NOT_NULL(cfg); + + cbm_set_user_lang_config(cfg); + CBMLanguage lang; + bool ok = shebang_probe("cbm_disc_dotd_uc", "foo-abc123.d", CARGO_DEP_INFO, &lang); + cbm_set_user_lang_config(NULL); + cbm_userconfig_free(cfg); + ASSERT(ok); + ASSERT_EQ(lang, CBM_LANG_MAKEFILE); + PASS(); +} + /* ── Suite ─────────────────────────────────────────────────────── */ SUITE(discover) { @@ -2082,6 +2126,10 @@ SUITE(discover) { RUN_TEST(shebang_embedded_nul_unindexed); RUN_TEST(shebang_oversized_first_line_unindexed); + /* .d: D source vs make/cargo dep-info */ + RUN_TEST(discover_d_dep_info_not_indexed_as_dlang); + RUN_TEST(discover_d_user_override_skips_dep_info_probe); + /* Integration tests (cross-platform) */ RUN_TEST(discover_simple); RUN_TEST(discover_wide_sibling_fanout_exceeds_initial_walk_stack); diff --git a/tests/test_language.c b/tests/test_language.c index dc2bcf7d36..d9d38429f8 100644 --- a/tests/test_language.c +++ b/tests/test_language.c @@ -835,6 +835,55 @@ TEST(lang_cfc_default_on_read_fail) { PASS(); } +/* ── .d: D source vs make/cargo dep-info ───────────────────────── */ + +/* Write content to a temp .d file and return cbm_disambiguate_d() for it. A + * setup failure returns -1 so neither a D nor a COUNT expectation can pass + * vacuously. */ +static int disambiguate_d_content(const char *content) { + char path[256]; + snprintf(path, sizeof(path), "%s/test_lang_dotd.d", cbm_tmpdir()); + if (!write_probe_file(path, content)) { + return -1; + } + CBMLanguage lang = cbm_disambiguate_d(path); + remove(path); + return (int)lang; +} + +TEST(lang_d_dep_info_unsupported) { + /* cargo (absolute target), gcc -MD continuation, CMake object rule, Windows + * drive letter (not the rule colon), escaped space (one target, not "app"), + * and the GNU make manual's "foo.o foo.d : ..." (two targets, spaced colon). */ + ASSERT_EQ(disambiguate_d_content("/home/u/t/debug/deps/foo-abc123.d: src/lib.rs src/a.rs\n" + "\nsrc/lib.rs:\n"), + CBM_LANG_COUNT); + ASSERT_EQ(disambiguate_d_content("foo.o: foo.c /usr/include/stdio.h \\\n" + " /usr/include/features.h\n"), + CBM_LANG_COUNT); + ASSERT_EQ(disambiguate_d_content("CMakeFiles/x.dir/a.cpp.o: \\\n /src/a.cpp\n"), + CBM_LANG_COUNT); + ASSERT_EQ(disambiguate_d_content("C:/b/foo.o: C:/s/foo.c \\\r\n C:/s/foo.h\r\n"), + CBM_LANG_COUNT); + ASSERT_EQ(disambiguate_d_content("out/my\\ app: main.c\n"), CBM_LANG_COUNT); + ASSERT_EQ(disambiguate_d_content("foo.o foo.d : foo.c defs.h\n"), CBM_LANG_COUNT); + PASS(); +} + +TEST(lang_d_source_stays_dlang) { + ASSERT_EQ(disambiguate_d_content("module a;\n"), CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("import std.stdio : writeln;\n"), CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("@safe:\nvoid f() {}\n"), CBM_LANG_DLANG); + /* A leading comment is D, even DUB's "/+ dub.sdl:" single-file recipe. */ + ASSERT_EQ(disambiguate_d_content("/+ dub.sdl:\n name \"hello\"\n+/\nvoid main() {}\n"), + CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("enum E : ubyte {\n a,\n}\n"), CBM_LANG_DLANG); + /* Default on doubt: empty or unreadable. */ + ASSERT_EQ(disambiguate_d_content(""), CBM_LANG_DLANG); + ASSERT_EQ(cbm_disambiguate_d("/tmp/nonexistent_file_12345.d"), CBM_LANG_DLANG); + PASS(); +} + /* --- New languages (auto-generated) --- */ TEST(lang_ext_solidity) { ASSERT_EQ(cbm_language_for_extension(".sol"), CBM_LANG_SOLIDITY); @@ -1488,6 +1537,8 @@ SUITE(language) { RUN_TEST(lang_frm_form_stays_form); RUN_TEST(lang_res_binary_resource_unsupported); RUN_TEST(lang_res_rescript_stays_rescript); + RUN_TEST(lang_d_dep_info_unsupported); + RUN_TEST(lang_d_source_stays_dlang); /* Go test ports */ /* New languages */ From 9140e7dfbe2e4942428e017cab8efef1f9de7f84 Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Thu, 1 Oct 2026 12:55:02 -0500 Subject: [PATCH 2/5] test(pipeline): dep-info .d files leave no File, Module or Folder nodes Run the full pipeline over a cargo dep-info file alone under its own directory, and a gcc -MD file beside real D source. The dep-info files get no File or Module node, their directory chain gets no Folder node, and the D file keeps its File, Module and Folder. With the .d probe disabled the test fails with three Folder nodes under the dep-info directory. Signed-off-by: Nelson Spence --- tests/test_pipeline.c | 66 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 03a5cad70a..3b84a5d748 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -631,6 +631,71 @@ TEST(pipeline_structure_nodes) { PASS(); } +/* Number of `label` nodes whose file_path starts with `prefix`, or -1. */ +static int count_nodes_with_path_prefix(cbm_store_t *s, const char *project, const char *label, + const char *prefix) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_label(s, project, label, &nodes, &count) != CBM_STORE_OK) { + return -1; + } + int hits = 0; + for (int i = 0; i < count; i++) { + if (nodes[i].file_path && strncmp(nodes[i].file_path, prefix, strlen(prefix)) == 0) { + hits++; + } + } + cbm_store_free_nodes(nodes, count); + return hits; +} + +/* Make-style dep-info .d files are dropped at discovery, so the graph holds no + * File or Module node for them and no Folder for a directory that held only + * dep-info. Real D source beside a dep-info file keeps its nodes and folder. */ +TEST(pipeline_d_dep_info_leaves_no_nodes) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_dotd_graph_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + char repo[512]; + snprintf(repo, sizeof(repo), "%s/repo", tmp); + write_temp_file(repo, "cargo-target/debug/deps/app-0123abcd.d", + "/r/cargo-target/debug/deps/app-0123abcd.d: src/main.rs\n\nsrc/main.rs:\n"); + write_temp_file(repo, "src/app.d", "module app;\n\nvoid main() {}\n"); + write_temp_file(repo, "src/app.o.d", "src/app.o: src/app.d\n"); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/test.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + const char *project = cbm_pipeline_project_name(p); + + int dep_folders = count_nodes_with_path_prefix(s, project, "Folder", "cargo-target"); + int dep_files = count_nodes_with_path_prefix(s, project, "File", "cargo-target"); + int dep_modules = count_nodes_with_path_prefix(s, project, "Module", "cargo-target"); + int obj_files = count_nodes_with_path_prefix(s, project, "File", "src/app.o.d"); + int obj_modules = count_nodes_with_path_prefix(s, project, "Module", "src/app.o.d"); + int src_folders = count_nodes_with_path_prefix(s, project, "Folder", "src"); + int src_files = count_nodes_with_path_prefix(s, project, "File", "src/app.d"); + int src_modules = count_nodes_with_path_prefix(s, project, "Module", "src/app.d"); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + + ASSERT_EQ(dep_folders, 0); + ASSERT_EQ(dep_files, 0); + ASSERT_EQ(dep_modules, 0); + ASSERT_EQ(obj_files, 0); + ASSERT_EQ(obj_modules, 0); + ASSERT_EQ(src_folders, 1); + ASSERT_EQ(src_files, 1); + ASSERT_EQ(src_modules, 1); + PASS(); +} + /* Issue #516: an ADR stored via manage_adr (project_summaries) must survive a * full re-index. A full re-index deletes the DB and rebuilds it from the graph * buffer, which writes an empty project_summaries table; the fix captures the @@ -16218,6 +16283,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_sql_dump_graph_matches_the_full_parse_issue1735); RUN_TEST(pipeline_spill_resolves_namespace_imports_like_memory); RUN_TEST(pipeline_structure_nodes); + RUN_TEST(pipeline_d_dep_info_leaves_no_nodes); RUN_TEST(pipeline_committed_counts_match_persisted); RUN_TEST(pipeline_adr_survives_full_reindex); RUN_TEST(pipeline_export_error_snapshot_on_artifact_failure); From ceb729d8a36b9a02778d86d0b209c7decbe58dd5 Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Thu, 1 Oct 2026 14:09:50 -0500 Subject: [PATCH 3/5] fix(discover): keep D whose first colon follows an attribute or comment is_dep_rule_line took any '.' or '/' before the rule colon as a path, so valid D such as "@1.0:" (a float UDA) and "public/**/:" (a comment between tokens) was dropped as dep-info. A target holding '@' or a comment opener ("/*", "/+", "//") is now D. Over 12,062 real D files and 4,494 real files under deps/ dirs, no classification changes. A dep-info file whose target path contains '@' now stays D, as it does on main. Signed-off-by: Nelson Spence --- src/discover/language.c | 9 +++++++-- tests/test_language.c | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/discover/language.c b/src/discover/language.c index 66de4f4df9..0d6d3facad 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -1342,7 +1342,9 @@ static bool is_rule_colon(const char *p, bool whole_file) { * look like paths (contain '/', '\' or '.'); a backslash-escaped space stays * inside its target. D source fails this: "public:", "@safe:", "extern(C):" * and "import a.b : c" all have a non-path word or D punctuation before the - * colon. */ + * colon. An attribute ('@', as in "@1.0:") or a comment between tokens + * ("public/+ +/:") is D even with a '.' or '/' before the colon, so a target + * holding either is never dep-info. */ static bool is_dep_rule_line(const char *p, bool whole_file) { bool in_target = false; bool path_like = false; @@ -1359,7 +1361,10 @@ static bool is_dep_rule_line(const char *p, bool whole_file) { path_like = false; continue; } - if (strchr("(){};=\"',", *p)) { + if (strchr("(){};=\"',@", *p)) { + return false; + } + if (*p == '/' && (p[SKIP_ONE] == '*' || p[SKIP_ONE] == '+' || p[SKIP_ONE] == '/')) { return false; } in_target = true; diff --git a/tests/test_language.c b/tests/test_language.c index d9d38429f8..d93eb0ecb1 100644 --- a/tests/test_language.c +++ b/tests/test_language.c @@ -878,6 +878,14 @@ TEST(lang_d_source_stays_dlang) { ASSERT_EQ(disambiguate_d_content("/+ dub.sdl:\n name \"hello\"\n+/\nvoid main() {}\n"), CBM_LANG_DLANG); ASSERT_EQ(disambiguate_d_content("enum E : ubyte {\n a,\n}\n"), CBM_LANG_DLANG); + /* A float UDA ("@ FloatLiteral") has a '.' before its colon, alone or glued + * to a keyword. */ + ASSERT_EQ(disambiguate_d_content("@1.0:\nvoid f() {}\n"), CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("nothrow@1.0:\nvoid f() {}\n"), CBM_LANG_DLANG); + /* A comment between tokens puts a '/' before the colon. */ + ASSERT_EQ(disambiguate_d_content("public/**/:\nvoid f() {}\n"), CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("public/+ +/:\nvoid f() {}\n"), CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("public//note: x\nvoid f() {}\n"), CBM_LANG_DLANG); /* Default on doubt: empty or unreadable. */ ASSERT_EQ(disambiguate_d_content(""), CBM_LANG_DLANG); ASSERT_EQ(cbm_disambiguate_d("/tmp/nonexistent_file_12345.d"), CBM_LANG_DLANG); From 006f2a991c6a8e0d141750d53959150147f876ad Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Thu, 1 Oct 2026 14:40:06 -0500 Subject: [PATCH 4/5] fix(discover): split dep-rule targets on every D whitespace is_dep_rule_line split targets only on space, tab and CR, so valid D such as "import\fstd.stdio : writeln;" kept the keyword and the dotted module name in one path-like target and was dropped as dep-info. Vertical tab, form feed and the UTF-8 separators U+2028/U+2029 now split targets too. Over 12,062 real D files and 4,494 real files under deps/ dirs, no classification changes. Signed-off-by: Nelson Spence --- src/discover/language.c | 18 +++++++++++++++++- tests/test_language.c | 10 ++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/discover/language.c b/src/discover/language.c index 0d6d3facad..c0f6f18944 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -1338,6 +1338,20 @@ static bool is_rule_colon(const char *p, bool whole_file) { (next == '\0' && whole_file)); } +/* Bytes of D whitespace at p, or 0: space, tab, vertical tab, form feed, CR + * and the UTF-8 line and paragraph separators U+2028/U+2029, all of which split + * D tokens. '\n' ends the line and is the caller's. */ +static size_t d_space_len(const char *p) { + if (*p == ' ' || *p == '\t' || *p == '\v' || *p == '\f' || *p == '\r') { + return SKIP_ONE; + } + if (strncmp(p, "\xE2\x80\xA8", SLEN("\xE2\x80\xA8")) == 0 || + strncmp(p, "\xE2\x80\xA9", SLEN("\xE2\x80\xA9")) == 0) { + return SLEN("\xE2\x80\xA8"); + } + return 0; +} + /* True if the line at p is a make rule "target...: prereq..." whose targets all * look like paths (contain '/', '\' or '.'); a backslash-escaped space stays * inside its target. D source fails this: "public:", "@safe:", "extern(C):" @@ -1353,12 +1367,14 @@ static bool is_dep_rule_line(const char *p, bool whole_file) { if (is_rule_colon(p, whole_file)) { return in_target ? path_like : any_target; } - if (*p == ' ' || *p == '\t' || *p == '\r') { + size_t space = d_space_len(p); + if (space) { if (in_target && !path_like) { return false; } in_target = false; path_like = false; + p += space - SKIP_ONE; continue; } if (strchr("(){};=\"',@", *p)) { diff --git a/tests/test_language.c b/tests/test_language.c index d93eb0ecb1..9b3bdc0a99 100644 --- a/tests/test_language.c +++ b/tests/test_language.c @@ -886,6 +886,16 @@ TEST(lang_d_source_stays_dlang) { ASSERT_EQ(disambiguate_d_content("public/**/:\nvoid f() {}\n"), CBM_LANG_DLANG); ASSERT_EQ(disambiguate_d_content("public/+ +/:\nvoid f() {}\n"), CBM_LANG_DLANG); ASSERT_EQ(disambiguate_d_content("public//note: x\nvoid f() {}\n"), CBM_LANG_DLANG); + /* Every D whitespace splits tokens, not only space, tab and CR: form feed, + * vertical tab, U+2028 and U+2029. */ + ASSERT_EQ(disambiguate_d_content("import\fstd.stdio : writeln;\n"), CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("import\vstd.stdio : writeln;\n"), CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("import\xE2\x80\xA8" + "std.stdio : writeln;\n"), + CBM_LANG_DLANG); + ASSERT_EQ(disambiguate_d_content("import\xE2\x80\xA9" + "std.stdio : writeln;\n"), + CBM_LANG_DLANG); /* Default on doubt: empty or unreadable. */ ASSERT_EQ(disambiguate_d_content(""), CBM_LANG_DLANG); ASSERT_EQ(cbm_disambiguate_d("/tmp/nonexistent_file_12345.d"), CBM_LANG_DLANG); From 66ff5b86a3f3219fcddc9b69f75a2036a96c8ae9 Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Fri, 9 Oct 2026 12:12:49 -0500 Subject: [PATCH 5/5] test(language): cover the .d rule in the shared classifier tests lang_probe_bytes_follow_the_name, lang_classify_matches_every_content_rule and the unreadable-content test now include .d: a 4 KB probe, a cargo dep-info line that is not indexed and D source that stays D, through both cbm_language_classify and cbm_disambiguate_d. With lang_probe_d disabled, the first two fail, as does discover_d_dep_info_not_indexed_as_dlang. Signed-off-by: Nelson Spence --- tests/test_language.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_language.c b/tests/test_language.c index 0d73720e38..43388b3d63 100644 --- a/tests/test_language.c +++ b/tests/test_language.c @@ -1415,6 +1415,7 @@ TEST(lang_probe_bytes_follow_the_name) { ASSERT_EQ(cbm_language_probe_bytes("foo.inc"), 4096); ASSERT_EQ(cbm_language_probe_bytes("Form1.frm"), 4096); ASSERT_EQ(cbm_language_probe_bytes("App.res"), 4096); + ASSERT_EQ(cbm_language_probe_bytes("app.d"), 4096); ASSERT_EQ(cbm_language_probe_bytes("Widget.cfc"), 16384); ASSERT_EQ(cbm_language_probe_bytes("pom.xml"), 255); /* An unknown name falls back to a shebang probe of the first line. */ @@ -1456,6 +1457,9 @@ TEST(lang_classify_matches_every_content_rule) { {"p.frm", "#procedure foo\nLocal F = a;\n", CBM_LANG_FORM, cbm_disambiguate_frm}, {"q.res", "RSRC\0\0\0\1binary", CBM_LANG_COUNT, cbm_disambiguate_res}, {"r.res", "let x = 1\n", CBM_LANG_RESCRIPT, cbm_disambiguate_res}, + {"dep.d", "target/debug/deps/app-0a1b2c.d: src/main.rs src/lib.rs\n", CBM_LANG_COUNT, + cbm_disambiguate_d}, + {"app.d", "module app;\nimport std.stdio : writeln;\n", CBM_LANG_DLANG, cbm_disambiguate_d}, {"s.xml", "\n\n", CBM_LANG_OBJECTSCRIPT_EXPORT, NULL}, {"t.xml", "\n\n", CBM_LANG_XML, NULL}, @@ -1503,6 +1507,7 @@ TEST(lang_classify_unreadable_content_keeps_the_name_default) { ASSERT_EQ(cbm_language_classify("a.cfc", NULL, 0, false, false), CBM_LANG_CFSCRIPT); ASSERT_EQ(cbm_language_classify("a.frm", NULL, 0, false, false), CBM_LANG_FORM); ASSERT_EQ(cbm_language_classify("a.res", NULL, 0, false, false), CBM_LANG_RESCRIPT); + ASSERT_EQ(cbm_language_classify("a.d", NULL, 0, false, false), CBM_LANG_DLANG); ASSERT_EQ(cbm_language_classify("a.xml", NULL, 0, false, false), CBM_LANG_XML); ASSERT_EQ(cbm_language_classify("run-tests", NULL, 0, false, false), CBM_LANG_COUNT); ASSERT_EQ(cbm_language_classify("main.go", NULL, 0, false, false), CBM_LANG_GO);