Skip to content

Commit ab074be

Browse files
authored
fix(workspace): clamp CLI include paths to member folder (#33949)
When `deno fmt .` or `deno lint .` is run from the workspace root, every member's FilePatterns inherits the CLI include `Path(cwd)`. Later, in FileCollector::collect_file_patterns, FilePatterns::split_by_base uses each include path as the walk base, so every member ends up traversing the entire workspace — O(N members × all files) work, duplicate output, and stack overflow on large workspaces like denoland/std. Clamp any include path that is a proper parent of a member's folder down to the member's folder when constructing per-member FilePatterns in split_cli_args_by_deno_json_folder. Each member then only walks its own subtree. Fixes #30915
1 parent 2588e14 commit ab074be

1 file changed

Lines changed: 97 additions & 3 deletions

File tree

libs/config/workspace/mod.rs

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,10 @@ impl Workspace {
15341534
for folder_url in matched_folder_urls {
15351535
let entry = results.entry((*folder_url).clone());
15361536
let folder_path = url_to_file_path(folder_url).unwrap();
1537+
let scoped_include = pattern
1538+
.include
1539+
.as_ref()
1540+
.map(|i| scope_include_to_folder(i, &folder_path));
15371541
match entry {
15381542
indexmap::map::Entry::Occupied(entry) => {
15391543
let entry = entry.into_mut();
@@ -1545,7 +1549,7 @@ impl Workspace {
15451549
}
15461550
match &mut entry.include {
15471551
Some(set) => {
1548-
if let Some(includes) = &pattern.include {
1552+
if let Some(includes) = &scoped_include {
15491553
for include in includes.inner() {
15501554
if !set.inner().contains(include) {
15511555
set.push(include.clone())
@@ -1554,7 +1558,7 @@ impl Workspace {
15541558
}
15551559
}
15561560
None => {
1557-
entry.include.clone_from(&pattern.include);
1561+
entry.include = scoped_include;
15581562
}
15591563
}
15601564
}
@@ -1565,7 +1569,7 @@ impl Workspace {
15651569
} else {
15661570
folder_path.clone()
15671571
},
1568-
include: pattern.include.clone(),
1572+
include: scoped_include,
15691573
exclude: pattern.exclude.clone(),
15701574
});
15711575
}
@@ -2796,6 +2800,30 @@ fn combine_patterns(
27962800
}
27972801
}
27982802

2803+
/// Restrict a CLI-supplied include set to a single workspace folder.
2804+
///
2805+
/// `split_by_base` uses each include path as the walk root, so an include path
2806+
/// that's a proper parent of the member's folder (e.g. `Path(cwd)` for member
2807+
/// `cwd/member-a`) would cause every member to traverse the entire workspace.
2808+
/// Clamp such paths down to the member's folder so each member only walks its
2809+
/// own subtree.
2810+
fn scope_include_to_folder(
2811+
include: &PathOrPatternSet,
2812+
folder_path: &Path,
2813+
) -> PathOrPatternSet {
2814+
let scoped = include
2815+
.inner()
2816+
.iter()
2817+
.map(|p| match p {
2818+
PathOrPattern::Path(path) if folder_path.starts_with(path) => {
2819+
PathOrPattern::Path(folder_path.to_path_buf())
2820+
}
2821+
_ => p.clone(),
2822+
})
2823+
.collect::<Vec<_>>();
2824+
PathOrPatternSet::new(scoped)
2825+
}
2826+
27992827
fn combine_files_config_with_cli_args(
28002828
files_config: &mut FilePatterns,
28012829
cli_arg_patterns: FilePatterns,
@@ -6032,6 +6060,72 @@ pub mod test {
60326060
});
60336061
}
60346062

6063+
// Regression test for https://github.com/denoland/deno/issues/30915 — running
6064+
// `deno fmt .` (or `deno lint .`) from the workspace root must not cause each
6065+
// member to walk the whole workspace.
6066+
#[test]
6067+
fn test_resolve_config_for_members_cli_include_workspace_root() {
6068+
let sys = InMemorySys::default();
6069+
sys.fs_insert_json(
6070+
root_dir().join("deno.json"),
6071+
json!({
6072+
"workspace": ["./member-a", "./member-b"],
6073+
}),
6074+
);
6075+
sys.fs_insert_json(root_dir().join("member-a/deno.json"), json!({}));
6076+
sys.fs_insert_json(root_dir().join("member-b/deno.json"), json!({}));
6077+
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
6078+
// Simulate CLI args from `deno fmt .` — base + include both pointing at the
6079+
// workspace root.
6080+
let cli_args = FilePatterns {
6081+
base: root_dir(),
6082+
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
6083+
root_dir(),
6084+
)])),
6085+
exclude: Default::default(),
6086+
};
6087+
let config_for_members = workspace_dir
6088+
.workspace
6089+
.resolve_fmt_config_for_members(&cli_args)
6090+
.unwrap();
6091+
let file_patterns = config_for_members
6092+
.into_iter()
6093+
.map(|(_ctx, config)| config.files)
6094+
.collect::<Vec<_>>();
6095+
assert_eq!(
6096+
file_patterns,
6097+
vec![
6098+
// Root walks from the workspace root, but excludes the member dirs.
6099+
FilePatterns {
6100+
base: root_dir(),
6101+
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
6102+
root_dir()
6103+
)])),
6104+
exclude: PathOrPatternSet::new(vec![
6105+
PathOrPattern::Path(root_dir().join("member-a")),
6106+
PathOrPattern::Path(root_dir().join("member-b")),
6107+
]),
6108+
},
6109+
// Each member's include is clamped to the member's own directory so
6110+
// that file collection walks only that subtree.
6111+
FilePatterns {
6112+
base: root_dir().join("member-a"),
6113+
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
6114+
root_dir().join("member-a")
6115+
)])),
6116+
exclude: Default::default(),
6117+
},
6118+
FilePatterns {
6119+
base: root_dir().join("member-b"),
6120+
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
6121+
root_dir().join("member-b")
6122+
)])),
6123+
exclude: Default::default(),
6124+
},
6125+
]
6126+
);
6127+
}
6128+
60356129
#[test]
60366130
fn test_resolve_config_for_members_excluded_member() {
60376131
let sys = InMemorySys::default();

0 commit comments

Comments
 (0)