Skip to content

Commit 2e7c64b

Browse files
authored
Merge pull request #811 from DeusData/distill/793-walk-exclusions
fix(pipeline): honor discovery exclusions in pkgmap/path-alias/envscan walks
2 parents bfd9b89 + c3d9a65 commit 2e7c64b

10 files changed

Lines changed: 285 additions & 27 deletions

src/pipeline/pass_envscan.c

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,16 +363,22 @@ static int scan_env_file(const char *full_path, const char *rel, file_type_t ft,
363363
/* Process a single directory entry for env scanning. Returns bindings added. */
364364
static int process_env_entry(cbm_dirent_t *ent, const char *dir_path, const char *root_path,
365365
cbm_env_binding_t *out, int max_out, char path_stack[][CBM_SZ_512],
366-
int *stack_top) {
366+
int *stack_top, char **excluded_dirs, int excluded_count) {
367367
char full_path[CBM_SZ_512];
368368
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, ent->name);
369+
const char *rel = full_path + strlen(root_path);
370+
while (*rel == '/') {
371+
rel++;
372+
}
369373

370374
struct stat st;
371375
if (stat(full_path, &st) != 0) {
372376
return 0;
373377
}
374378
if (S_ISDIR(st.st_mode)) {
375-
if (!is_ignored_dir(ent->name) && *stack_top < CBM_SZ_256) {
379+
if (!is_ignored_dir(ent->name) &&
380+
!cbm_pipeline_relpath_is_excluded(rel, excluded_dirs, excluded_count) &&
381+
*stack_top < CBM_SZ_256) {
376382
strncpy(path_stack[*stack_top], full_path, sizeof(path_stack[0]) - 1);
377383
path_stack[*stack_top][sizeof(path_stack[0]) - SKIP_ONE] = '\0';
378384
(*stack_top)++;
@@ -386,14 +392,11 @@ static int process_env_entry(cbm_dirent_t *ent, const char *dir_path, const char
386392
if (ft == FT_UNKNOWN) {
387393
return 0;
388394
}
389-
const char *rel = full_path + strlen(root_path);
390-
while (*rel == '/') {
391-
rel++;
392-
}
393395
return scan_env_file(full_path, rel, ft, out, max_out);
394396
}
395397

396-
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out) {
398+
int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t *out, int max_out,
399+
char **excluded_dirs, int excluded_count) {
397400
if (!root_path || !out || max_out <= 0) {
398401
return 0;
399402
}
@@ -418,9 +421,13 @@ int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int
418421
cbm_dirent_t *ent;
419422
while ((ent = cbm_readdir(d)) && count < max_out) {
420423
count += process_env_entry(ent, dir_path, root_path, out + count, max_out - count,
421-
path_stack, &stack_top);
424+
path_stack, &stack_top, excluded_dirs, excluded_count);
422425
}
423426
cbm_closedir(d);
424427
}
425428
return count;
426429
}
430+
431+
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out) {
432+
return cbm_scan_project_env_urls_excluded(root_path, out, max_out, NULL, 0);
433+
}

src/pipeline/pass_parallel.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ static void merge_pkg_entries(cbm_pipeline_ctx_t *ctx, cbm_pkg_entries_t *pkg_en
818818
* by the main discoverer (package.json, composer.json — in
819819
* IGNORED_JSON_FILES) still feed pkgmap. Append into worker 0's
820820
* array so the existing merge below sees them. */
821-
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0]);
821+
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0], ctx->excluded_dirs, ctx->excluded_count);
822822
cbm_pipeline_set_pkgmap(cbm_pkgmap_build(pkg_entries, worker_count, ctx->project_name));
823823
for (int i = 0; i < worker_count; i++) {
824824
cbm_pkg_entries_free(&pkg_entries[i]);

src/pipeline/pass_pkgmap.c

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ static bool pkgmap_is_reparse_point(const char *abs_path) {
845845
* it hang. On Windows we additionally skip reparse points before
846846
* descending as a best-effort early-out. */
847847
static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_entries_t *entries,
848-
int depth) {
848+
int depth, char **excluded_dirs, int excluded_count) {
849849
if (depth >= PKGMAP_WALK_MAX_DEPTH) {
850850
cbm_log_info("pkgmap.walk", "depth_cap", rel_dir && rel_dir[0] ? rel_dir : ".");
851851
return 0;
@@ -874,7 +874,8 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
874874
continue;
875875
}
876876
if (S_ISDIR(st.st_mode)) {
877-
if (cbm_should_skip_dir(name, CBM_MODE_FULL)) {
877+
if (cbm_should_skip_dir(name, CBM_MODE_FULL) ||
878+
cbm_pipeline_relpath_is_excluded(rel_path, excluded_dirs, excluded_count)) {
878879
continue;
879880
}
880881
#ifdef _WIN32
@@ -886,7 +887,8 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
886887
continue;
887888
}
888889
#endif
889-
parsed += pkgmap_walk_dir(abs_path, rel_path, entries, depth + 1);
890+
parsed += pkgmap_walk_dir(abs_path, rel_path, entries, depth + 1, excluded_dirs,
891+
excluded_count);
890892
continue;
891893
}
892894
if (!S_ISREG(st.st_mode)) {
@@ -920,11 +922,12 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
920922
* Windows reparse points, so it cannot hang on directory junctions.
921923
* This is what lets bare workspace imports (e.g. "@org/pkg" declared in
922924
* an ignored package.json) resolve on Windows as well as POSIX. */
923-
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries) {
925+
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char **excluded_dirs,
926+
int excluded_count) {
924927
if (!repo_path || !entries) {
925928
return 0;
926929
}
927-
int parsed = pkgmap_walk_dir(repo_path, "", entries, 0);
930+
int parsed = pkgmap_walk_dir(repo_path, "", entries, 0, excluded_dirs, excluded_count);
928931
cbm_log_info("pkgmap.scan_repo", "manifests", pkgmap_itoa(parsed));
929932
return parsed;
930933
}
@@ -961,7 +964,8 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file
961964
* (the canonical case: package.json, which is in IGNORED_JSON_FILES).
962965
* Falls back to the files[]-only behaviour if repo_path is NULL. */
963966
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
964-
int file_count, const char *project_name) {
967+
int file_count, const char *project_name,
968+
char **excluded_dirs, int excluded_count) {
965969
cbm_pkg_entries_t entries;
966970
cbm_pkg_entries_init(&entries);
967971

@@ -985,7 +989,7 @@ CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_i
985989
free(source);
986990
}
987991

988-
int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries);
992+
int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries, excluded_dirs, excluded_count);
989993
cbm_log_info("pkgmap.scan", "manifests_from_files", pkgmap_itoa(from_files),
990994
"manifests_from_walk", pkgmap_itoa(from_walk), "entries",
991995
pkgmap_itoa(entries.count));

src/pipeline/path_alias.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
#include "pipeline/path_alias.h"
2222

23+
#include "pipeline/pipeline_internal.h"
24+
2325
#include "foundation/compat.h"
2426
#include "foundation/compat_fs.h"
2527
#include "foundation/constants.h"
@@ -380,7 +382,8 @@ static const char *const TS_CONFIG_NAMES[] = {"tsconfig.json", "jsconfig.json"};
380382
enum { TS_CONFIG_NAMES_COUNT = 2 };
381383

382384
static void find_alias_files(const char *abs_dir, const char *rel_dir, alias_config_hit_t *out,
383-
int *count, int max_count, int depth) {
385+
int *count, int max_count, int depth, char **excluded_dirs,
386+
int excluded_count) {
384387
if (*count >= max_count || depth > CBM_PATH_ALIAS_MAX_DEPTH) {
385388
return;
386389
}
@@ -422,12 +425,18 @@ static void find_alias_files(const char *abs_dir, const char *rel_dir, alias_con
422425
} else {
423426
snprintf(child_rel, sizeof(child_rel), "%s/%s", rel_dir, name);
424427
}
425-
find_alias_files(child_abs, child_rel, out, count, max_count, depth + 1);
428+
if (cbm_pipeline_relpath_is_excluded(child_rel, excluded_dirs, excluded_count)) {
429+
continue;
430+
}
431+
find_alias_files(child_abs, child_rel, out, count, max_count, depth + 1, excluded_dirs,
432+
excluded_count);
426433
}
427434
cbm_closedir(d);
428435
}
429436

430-
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
437+
cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_path,
438+
char **excluded_dirs,
439+
int excluded_count) {
431440
if (!repo_path) {
432441
return NULL;
433442
}
@@ -436,7 +445,8 @@ cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
436445
return NULL;
437446
}
438447
int count = 0;
439-
find_alias_files(repo_path, "", hits, &count, CBM_PATH_ALIAS_MAX_FILES, 0);
448+
find_alias_files(repo_path, "", hits, &count, CBM_PATH_ALIAS_MAX_FILES, 0, excluded_dirs,
449+
excluded_count);
440450
if (count >= CBM_PATH_ALIAS_MAX_FILES) {
441451
cbm_log_warn("path_alias.files.cap_hit", "repo", repo_path, "kept", "256_of_more");
442452
}
@@ -479,6 +489,10 @@ cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
479489
return coll;
480490
}
481491

492+
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
493+
return cbm_load_path_aliases_excluded(repo_path, NULL, 0);
494+
}
495+
482496
const cbm_path_alias_map_t *cbm_path_alias_find_for_file(const cbm_path_alias_collection_t *coll,
483497
const char *rel_path) {
484498
if (!coll || !rel_path) {

src/pipeline/path_alias.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ typedef struct {
7272
* are found (also NULL on out-of-memory). Caller frees with
7373
* cbm_path_alias_collection_free. */
7474
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path);
75+
cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_path,
76+
char **excluded_dirs,
77+
int excluded_count);
7578

7679
/* Free a collection produced by cbm_load_path_aliases. NULL-safe. */
7780
void cbm_path_alias_collection_free(cbm_path_alias_collection_t *coll);

src/pipeline/pipeline.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,9 @@ static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
737737
* Use the repo-walking variant so manifests filtered out by the main
738738
* discoverer (package.json, composer.json) still feed pkgmap and let
739739
* workspace imports like `@my/pkg` resolve to their target Module. */
740-
cbm_pipeline_set_pkgmap(
741-
cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count, ctx->project_name));
740+
cbm_pipeline_set_pkgmap(cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count,
741+
ctx->project_name, ctx->excluded_dirs,
742+
ctx->excluded_count));
742743

743744
CBMFileResult **seq_cache = (CBMFileResult **)calloc(file_count, sizeof(CBMFileResult *));
744745
if (seq_cache) {
@@ -1261,7 +1262,8 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
12611262

12621263
/* Phase 2b: Load build-tool path aliases (tsconfig/jsconfig today). NULL
12631264
* when no usable configs are found — non-TS projects pay nothing. */
1264-
path_aliases = cbm_load_path_aliases(p->repo_path);
1265+
path_aliases =
1266+
cbm_load_path_aliases_excluded(p->repo_path, p->excluded_dirs, p->excluded_count);
12651267

12661268
/* Build shared context for pass functions */
12671269
cbm_pipeline_ctx_t ctx = {
@@ -1273,6 +1275,8 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
12731275
.pipeline = p, /* so passes can record per-file skips (Track B) */
12741276
.mode = (int)p->mode,
12751277
.path_aliases = path_aliases,
1278+
.excluded_dirs = p->excluded_dirs,
1279+
.excluded_count = p->excluded_count,
12761280
};
12771281

12781282
rc = run_extraction_phase(p, &ctx, files, file_count);

src/pipeline/pipeline_incremental.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,16 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
806806
cbm_log_info("incremental.registry_seed", "symbols", itoa_buf(cbm_registry_size(registry)),
807807
"elapsed_ms", itoa_buf((int)elapsed_ms(t)));
808808

809-
cbm_path_alias_collection_t *path_aliases = cbm_load_path_aliases(cbm_pipeline_repo_path(p));
809+
/* Discovery exclusions (gitignore + skip dirs) captured by the run that
810+
* routed here. Borrowed from the pipeline so the auxiliary repo walks
811+
* (pkgmap via merge_pkg_entries, path aliases) skip excluded subtrees on
812+
* incremental runs too — same borrow as the full path (#792/#804). */
813+
char **excluded_dirs = NULL;
814+
int excluded_count = 0;
815+
cbm_pipeline_get_excluded(p, &excluded_dirs, &excluded_count);
816+
817+
cbm_path_alias_collection_t *path_aliases =
818+
cbm_load_path_aliases_excluded(cbm_pipeline_repo_path(p), excluded_dirs, excluded_count);
810819

811820
cbm_pipeline_ctx_t ctx = {
812821
.project_name = project,
@@ -817,6 +826,8 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
817826
.pipeline = p, /* so passes can record per-file skips (Track B) */
818827
.mode = cbm_pipeline_get_mode(p),
819828
.path_aliases = path_aliases,
829+
.excluded_dirs = excluded_dirs,
830+
.excluded_count = excluded_count,
820831
};
821832

822833
for (int i = 0; i < ci; i++) {

src/pipeline/pipeline_internal.h

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "cbm.h"
1717
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
1818
#include <stdatomic.h>
19+
#include <string.h>
1920

2021
/* ── Shared pipeline constants ─────────────────────────────────── */
2122

@@ -81,8 +82,30 @@ typedef struct {
8182
* configs are an easy follow-on). NULL when no usable configs were found.
8283
* Owned by pipeline.c / pipeline_incremental.c. */
8384
const cbm_path_alias_collection_t *path_aliases;
85+
86+
/* Directory subtrees excluded during discovery. Borrowed from pipeline.c. */
87+
char **excluded_dirs;
88+
int excluded_count;
8489
} cbm_pipeline_ctx_t;
8590

91+
static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *const *excluded_dirs,
92+
int excluded_count) {
93+
if (!rel_path || rel_path[0] == '\0' || !excluded_dirs || excluded_count <= 0) {
94+
return 0;
95+
}
96+
for (int i = 0; i < excluded_count; i++) {
97+
const char *excluded = excluded_dirs[i];
98+
if (!excluded || excluded[0] == '\0') {
99+
continue;
100+
}
101+
size_t n = strlen(excluded);
102+
if (strncmp(rel_path, excluded, n) == 0 && (rel_path[n] == '\0' || rel_path[n] == '/')) {
103+
return SKIP_ONE;
104+
}
105+
}
106+
return 0;
107+
}
108+
86109
/* Get the current pipeline's package map (NULL if none). */
87110
CBMHashTable *cbm_pipeline_get_pkgmap(void);
88111
void cbm_pipeline_set_pkgmap(CBMHashTable *map);
@@ -131,9 +154,11 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou
131154
const char *project_name);
132155

133156
/* Build pkgmap by reading manifest files from the files array (sequential path). */
134-
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries);
157+
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char **excluded_dirs,
158+
int excluded_count);
135159
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
136-
int file_count, const char *project_name);
160+
int file_count, const char *project_name,
161+
char **excluded_dirs, int excluded_count);
137162
CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count,
138163
const char *project_name);
139164

@@ -530,8 +555,14 @@ typedef struct {
530555
/* Scan a project directory for environment variable assignments with URL values.
531556
* Walks the filesystem, scans Dockerfiles, shell scripts, .env, YAML, TOML,
532557
* Terraform, and .properties files. Filters out secrets.
533-
* Returns number of bindings written to out (up to max_out). */
558+
* Returns number of bindings written to out (up to max_out).
559+
* NOTE: this walker currently has no production callers — it is exercised
560+
* only by tests. The _excluded variant honors discovery exclusions for
561+
* consistency with the pkgmap/path-alias walks (#792); the plain variant
562+
* scans unexcluded (NULL exclusion list). */
534563
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out);
564+
int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t *out, int max_out,
565+
char **excluded_dirs, int excluded_count);
535566

536567
/* ── Incremental pipeline (pipeline_incremental.c) ───────────────── */
537568

tests/test_path_alias.c

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,62 @@ TEST(path_alias_loader_monorepo_dotdot_climb) {
336336
PASS();
337337
}
338338

339+
/* ── Loader honors discovery exclusions (#792) ─────────────────── */
340+
341+
/* find_alias_files must not descend into discovery-excluded subtrees.
342+
* Control run first (no exclusions → both configs collected) so the
343+
* exclusion assertion below cannot pass vacuously. */
344+
TEST(path_alias_loader_honors_discovery_exclusions) {
345+
char tmpl[256];
346+
snprintf(tmpl, sizeof(tmpl), "/tmp/cbm_palias_excl_XXXXXX");
347+
char *root = cbm_mkdtemp(tmpl);
348+
ASSERT_NOT_NULL(root);
349+
350+
char sub[512];
351+
snprintf(sub, sizeof(sub), "%s/big_generated", root);
352+
cbm_mkdir(sub);
353+
354+
char path[512];
355+
snprintf(path, sizeof(path), "%s/tsconfig.json", root);
356+
ASSERT_EQ(write_file(path,
357+
"{\n \"compilerOptions\": {\n \"paths\": {\n"
358+
" \"@root/*\": [\"shared/*\"]\n }\n }\n}\n"),
359+
0);
360+
snprintf(path, sizeof(path), "%s/big_generated/tsconfig.json", root);
361+
ASSERT_EQ(write_file(path,
362+
"{\n \"compilerOptions\": {\n \"paths\": {\n"
363+
" \"@gen/*\": [\"./src/*\"]\n }\n }\n}\n"),
364+
0);
365+
366+
/* Control: the unexcluded loader collects BOTH configs. */
367+
cbm_path_alias_collection_t *coll = cbm_load_path_aliases(root);
368+
ASSERT_NOT_NULL(coll);
369+
ASSERT_EQ(coll->count, 2);
370+
cbm_path_alias_collection_free(coll);
371+
372+
/* Excluding big_generated drops its config; the root one survives. */
373+
char *excluded[] = {(char *)"big_generated"};
374+
coll = cbm_load_path_aliases_excluded(root, excluded, 1);
375+
ASSERT_NOT_NULL(coll);
376+
ASSERT_EQ(coll->count, 1);
377+
const cbm_path_alias_map_t *m = cbm_path_alias_find_for_file(coll, "src/x.ts");
378+
ASSERT_NOT_NULL(m);
379+
char *r = cbm_path_alias_resolve(m, "@root/utils");
380+
ASSERT_NOT_NULL(r);
381+
ASSERT_STR_EQ(r, "shared/utils");
382+
free(r);
383+
cbm_path_alias_collection_free(coll);
384+
385+
snprintf(path, sizeof(path), "%s/big_generated/tsconfig.json", root);
386+
unlink(path);
387+
snprintf(path, sizeof(path), "%s/tsconfig.json", root);
388+
unlink(path);
389+
snprintf(path, sizeof(path), "%s/big_generated", root);
390+
rmdir(path);
391+
rmdir(root);
392+
PASS();
393+
}
394+
339395
/* ── Loader returns NULL when no configs found ─────────────────── */
340396

341397
TEST(path_alias_loader_no_configs) {
@@ -363,5 +419,6 @@ void suite_path_alias(void) {
363419
RUN_TEST(path_alias_find_for_file_nearest_ancestor);
364420
RUN_TEST(path_alias_loader_monorepo);
365421
RUN_TEST(path_alias_loader_monorepo_dotdot_climb);
422+
RUN_TEST(path_alias_loader_honors_discovery_exclusions);
366423
RUN_TEST(path_alias_loader_no_configs);
367424
}

0 commit comments

Comments
 (0)