Skip to content

Commit 731541f

Browse files
karthiknadigCopilot
andcommitted
fix: address glob expansion review feedback (PR #541)
Charge legacy brace alternatives before formatting, admit brace-only requests through the expansion budget, and preserve configured path priority during deduplication. Add regressions for each behavior without changing pinned terminal-glob semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 94f869c commit 731541f

3 files changed

Lines changed: 41 additions & 13 deletions

File tree

‎crates/pet-fs/src/glob.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ fn expand_braces_inner(pattern: &str, results: &mut Vec<String>) {
136136
.and_then(|open| pattern[open..].find('}').map(|close| (open, open + close)));
137137
if let Some((open, close)) = group {
138138
for alternative in pattern[open + 1..close].split(',').rev() {
139+
if steps == MAX_BRACE_EXPANSION_STEPS {
140+
log::warn!("Brace expansion exceeded its work limit, truncating '{pattern}'");
141+
return;
142+
}
143+
steps += 1;
139144
pending.push(format!(
140145
"{}{alternative}{}",
141146
&pattern[..open],
@@ -749,6 +754,14 @@ mod tests {
749754
));
750755
}
751756

757+
#[test]
758+
fn legacy_brace_expansion_charges_alternatives_before_formatting() {
759+
let pattern = format!("{{{}}}", vec!["a"; MAX_BRACE_EXPANSION_STEPS].join(","));
760+
assert!(expand_braces(&pattern).is_empty());
761+
assert!(expand_glob_pattern(&pattern).is_empty());
762+
assert!(!is_recursive_glob_pattern(&pattern));
763+
}
764+
752765
#[test]
753766
fn duplicate_brace_alternatives_count_toward_work_limit() {
754767
let within_limit = format!("{{{}}}", vec!["a"; MAX_BRACE_EXPANSION_STEPS - 2].join(","));

‎crates/pet/src/jsonrpc.rs‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use pet_core::{
2222
};
2323
use pet_env_var_path::get_search_paths_from_env_variables;
2424
use pet_fs::glob::{
25-
expand_glob_patterns_bounded, is_recursive_glob_pattern, GlobExpansionError,
25+
expand_glob_patterns_bounded, is_glob_pattern, is_recursive_glob_pattern, GlobExpansionError,
2626
DEFAULT_GLOB_EXPANSION_LIMITS,
2727
};
2828
use pet_fs::path::norm_case;
@@ -40,7 +40,7 @@ use pet_telemetry::report_inaccuracies_identified_after_resolving;
4040
use serde::{Deserialize, Serialize};
4141
use serde_json::json;
4242
use serde_json::{self, Value};
43-
use std::collections::BTreeMap;
43+
use std::collections::{BTreeMap, HashSet};
4444
use std::sync::atomic::{AtomicU64, Ordering};
4545
use std::time::Duration;
4646
use std::{
@@ -98,7 +98,7 @@ impl GlobExpansionAdmission {
9898
) -> Result<Option<GlobExpansionPermit>, String> {
9999
if !patterns
100100
.into_iter()
101-
.any(|pattern| pattern.to_string_lossy().contains(['*', '?', '[', ']']))
101+
.any(|pattern| is_glob_pattern(&pattern.to_string_lossy()))
102102
{
103103
return Ok(None);
104104
}
@@ -820,13 +820,12 @@ fn normalize_refresh_params(params: Value) -> Value {
820820
}
821821

822822
fn deduplicate_path_patterns(paths: &[PathBuf]) -> Vec<PathBuf> {
823-
let mut patterns = BTreeMap::new();
824-
for path in paths {
825-
patterns
826-
.entry(path.as_os_str().to_owned())
827-
.or_insert_with(|| path.clone());
828-
}
829-
patterns.into_values().collect()
823+
let mut seen = HashSet::new();
824+
paths
825+
.iter()
826+
.filter(|path| seen.insert(path.as_os_str()))
827+
.cloned()
828+
.collect()
830829
}
831830

832831
fn parse_refresh_options(params: Value) -> Result<RefreshOptions, serde_json::Error> {
@@ -1873,7 +1872,12 @@ mod tests {
18731872
.try_acquire_for_patterns(std::iter::once(&literal))
18741873
.unwrap()
18751874
.is_none());
1876-
for text in ["workspace/*", "workspace/**", "malformed["] {
1875+
for text in [
1876+
"workspace/*",
1877+
"workspace/**",
1878+
"malformed[",
1879+
"workspace/{a,b}",
1880+
] {
18771881
let pattern = PathBuf::from(text);
18781882
assert!(admission
18791883
.try_acquire_for_patterns(std::iter::once(&pattern))
@@ -1912,6 +1916,16 @@ mod tests {
19121916
.expect("refresh without paths must bypass glob admission");
19131917
}
19141918

1919+
#[test]
1920+
fn pattern_deduplication_preserves_configured_priority() {
1921+
let paths = [
1922+
PathBuf::from("z-priority"),
1923+
PathBuf::from("a-fallback"),
1924+
PathBuf::from("z-priority"),
1925+
];
1926+
assert_eq!(deduplicate_path_patterns(&paths), paths[..2]);
1927+
}
1928+
19151929
#[test]
19161930
fn pattern_deduplication_preserves_directory_only_globs() {
19171931
let patterns = [PathBuf::from("workspace/*"), PathBuf::from("workspace/*/")];

‎docs/JSONRPC.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,10 @@ limits return a JSON-RPC error (`-4`) and no partial refresh inventory. Limits a
194194
checked between filesystem entries; they are not a timeout and cannot interrupt an
195195
operating-system filesystem call already in progress.
196196

197-
PET admits at most two configure/refresh filesystem glob expansions concurrently.
198-
Requests without wildcard patterns do not consume these slots. Additional wildcard
197+
PET admits at most two configure/refresh glob or brace expansions concurrently.
198+
Requests containing only literal paths do not consume these slots. Additional expansion
199199
requests receive JSON-RPC error `-4` instead of creating an unbounded traversal queue.
200+
Input deduplication preserves the first occurrence order of configured paths.
200201

201202
## Refresh Progress Telemetry
202203

0 commit comments

Comments
 (0)