From 1bfe473a7678f20779b73504ab76d03a6febef8f Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Thu, 19 Mar 2026 18:07:27 -0500 Subject: [PATCH] fix(discover): address QA round 1 findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pipeline.c: inline userconfig cleanup on early-exit paths (discover failure and cancel check) so userconfig is always freed, fixing the resource leak when pipeline exits before reaching the cleanup label - userconfig.c: free partially-built entries array on global config parse failure to fix alloc-failure memory leak - language.c: probe user config for compound extensions (e.g. ".blade.php") by scanning from the first dot to the last, falling back to built-in single-extension lookup — preserves built-in behavior while correctly resolving user-defined compound extensions Co-Authored-By: Claude Sonnet 4.6 --- src/discover/language.c | 27 ++++++++++++++++++++++----- src/discover/userconfig.c | 4 ++++ src/pipeline/pipeline.c | 7 +++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/discover/language.c b/src/discover/language.c index 51b74c24c..ea07e9c2d 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -386,13 +386,30 @@ CBMLanguage cbm_language_for_filename(const char *filename) { } } - /* Fall back to extension-based lookup */ - const char *dot = strrchr(filename, '.'); - if (dot) { - return cbm_language_for_extension(dot); + /* Fall back to extension-based lookup. + * For compound extensions (e.g. ".blade.php") defined in the user config, + * scan from the first dot in the basename toward the last, checking user + * config at each position. Built-in extensions use the last dot only. */ + const char *last_dot = strrchr(filename, '.'); + if (!last_dot) { + return CBM_LANG_COUNT; } - return CBM_LANG_COUNT; + /* Probe user config for compound extensions (e.g. ".blade.php"). */ + const cbm_userconfig_t *ucfg = cbm_get_user_lang_config(); + if (ucfg) { + const char *p = strchr(filename, '.'); + while (p && p < last_dot) { + CBMLanguage lang = cbm_userconfig_lookup(ucfg, p); + if (lang != CBM_LANG_COUNT) { + return lang; + } + p = strchr(p + 1, '.'); + } + } + + /* Standard single-extension lookup (built-ins + user overrides). */ + return cbm_language_for_extension(last_dot); } const char *cbm_language_name(CBMLanguage lang) { diff --git a/src/discover/userconfig.c b/src/discover/userconfig.c index 66edef254..d9dd74982 100644 --- a/src/discover/userconfig.c +++ b/src/discover/userconfig.c @@ -304,6 +304,10 @@ cbm_userconfig_t *cbm_userconfig_load(const char *repo_path) { snprintf(global_path, sizeof(global_path), "%s/config.json", global_dir); if (load_config_file(global_path, &entries, &count) != 0) { + for (int i = 0; i < count; i++) { + free(entries[i].ext); + } + free(entries); free(cfg); return NULL; } diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 63ddeed10..e6ee73a69 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -322,6 +322,10 @@ int cbm_pipeline_run(cbm_pipeline_t *p) { int rc = cbm_discover(p->repo_path, &opts, &files, &file_count); if (rc != 0) { cbm_log_error("pipeline.err", "phase", "discover", "rc", itoa_buf(rc)); + cbm_discover_free(files, file_count); + cbm_set_user_lang_config(NULL); + cbm_userconfig_free(p->userconfig); + p->userconfig = NULL; return -1; } cbm_log_info("pipeline.discover", "files", itoa_buf(file_count), "elapsed_ms", @@ -329,6 +333,9 @@ int cbm_pipeline_run(cbm_pipeline_t *p) { if (check_cancel(p)) { cbm_discover_free(files, file_count); + cbm_set_user_lang_config(NULL); + cbm_userconfig_free(p->userconfig); + p->userconfig = NULL; return -1; }