From 7bcd2de892360545a83b5e7e7a2ec3da82e87d14 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 12 Jan 2024 20:36:51 -0500 Subject: [PATCH 1/8] feat: move globbing to deno_config --- Cargo.lock | 155 +++++++++++++++ Cargo.toml | 2 + src/glob.rs | 528 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 153 +++++++-------- 4 files changed, 748 insertions(+), 90 deletions(-) create mode 100644 src/glob.rs diff --git a/Cargo.lock b/Cargo.lock index b43faf5..3b5fc38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,11 +8,30 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "deno_config" version = "0.6.5" dependencies = [ "anyhow", + "glob", "indexmap", "jsonc-parser", "log", @@ -20,6 +39,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", + "tempfile", "url", ] @@ -35,6 +55,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -44,6 +80,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.14.0" @@ -86,6 +128,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + [[package]] name = "log" version = "0.4.20" @@ -126,6 +180,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "ryu" version = "1.0.15" @@ -174,6 +250,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -221,6 +310,72 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index fdc052d..17ff7c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,12 @@ anyhow = "1.0.57" indexmap = { version = "2", features = ["serde"] } jsonc-parser = { version = "0.23.0", features = ["serde"] } log = "0.4.20" +glob = "0.3.1" percent-encoding = "2.3.0" serde = { version = "1.0.149", features = ["derive"] } serde_json = "1.0.85" url = { version = "2.3.1" } [dev-dependencies] +tempfile = "3.4.0" pretty_assertions = "1.4.0" diff --git a/src/glob.rs b/src/glob.rs new file mode 100644 index 0000000..65139be --- /dev/null +++ b/src/glob.rs @@ -0,0 +1,528 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::Context; +use url::Url; +use indexmap::IndexMap; + +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub struct FilePatterns { + pub include: Option, + pub exclude: PathOrPatternSet, +} + +impl FilePatterns { + pub fn matches_specifier(&self, specifier: &Url) -> bool { + if specifier.scheme() != "file" { + return true; + } + let path = match specifier.to_file_path() { + Ok(path) => path, + Err(_) => return true, + }; + self.matches_path(&path) + } + + pub fn matches_path(&self, path: &Path) -> bool { + // Skip files in the exclude list. + if self.exclude.matches_path(path) { + return false; + } + + // Ignore files not in the include list if it's present. + self + .include + .as_ref() + .map(|m| m.matches_path(path)) + .unwrap_or(true) + } + + /// Creates a collection of `FilePatterns` by base where the containing patterns + /// are only the ones applicable to the base. + /// + /// The order these are returned in is the order that the directory traversal + /// should occur in. + pub fn split_by_base(&self) -> Vec<(PathBuf, Self)> { + let Some(include) = &self.include else { + return Vec::new(); + }; + + let mut include_paths = Vec::new(); + let mut include_patterns = Vec::new(); + for path_or_pattern in &include.0 { + match path_or_pattern { + PathOrPattern::Path(path) => include_paths.push((path.is_file(), path)), + PathOrPattern::Pattern(pattern) => include_patterns.push(pattern), + PathOrPattern::RemoteUrl(_) => {}, + } + } + let include_patterns_by_base_path = include_patterns.into_iter().fold( + IndexMap::new(), + |mut map: IndexMap<_, Vec<_>>, p| { + map.entry(p.base_path()).or_default().push(p); + map + }, + ); + let exclude_by_base_path = self + .exclude + .0 + .iter() + .filter_map(|s| Some((s.base_path()?, s))) + .collect::>(); + let get_applicable_excludes = + |is_file_path: bool, base_path: &PathBuf| -> Vec { + exclude_by_base_path + .iter() + .filter_map(|(exclude_base_path, exclude)| { + match exclude { + PathOrPattern::RemoteUrl(_) => None, + PathOrPattern::Path(exclude_path) => { + // For explicitly specified files, ignore when the exclude path starts + // with it. Regardless, include excludes that are on a sub path of the dir. + if is_file_path && base_path.starts_with(exclude_path) + || exclude_path.starts_with(base_path) + { + Some((*exclude).clone()) + } else { + None + } + } + PathOrPattern::Pattern(_) => { + // include globs that's are sub paths or a parent path + if exclude_base_path.starts_with(base_path) + || base_path.starts_with(exclude_base_path) + { + Some((*exclude).clone()) + } else { + None + } + } + } + }) + .collect::>() + }; + + let mut result = Vec::with_capacity( + include_paths.len() + include_patterns_by_base_path.len(), + ); + for (is_file, path) in include_paths { + let applicable_excludes = get_applicable_excludes(is_file, path); + result.push(( + path.clone(), + Self { + include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path( + path.clone(), + )])), + exclude: PathOrPatternSet::new(applicable_excludes), + }, + )); + } + + // todo(dsherret): This could be further optimized by not including + // patterns that will only ever match another base. + for base_path in include_patterns_by_base_path.keys() { + let applicable_excludes = get_applicable_excludes(false, base_path); + let mut applicable_includes = Vec::new(); + // get all patterns that apply to the current or ancestor directories + for path in base_path.ancestors() { + if let Some(patterns) = include_patterns_by_base_path.get(path) { + applicable_includes.extend( + patterns + .iter() + .map(|p| PathOrPattern::Pattern((*p).clone())), + ); + } + } + result.push(( + base_path.clone(), + Self { + include: Some(PathOrPatternSet::new(applicable_includes)), + exclude: PathOrPatternSet::new(applicable_excludes), + }, + )); + } + + // Sort by the longest base path first. This ensures that we visit opted into + // nested directories first before visiting the parent directory. The directory + // traverser will handle not going into directories it's already been in. + result.sort_by(|a, b| b.0.as_os_str().len().cmp(&a.0.as_os_str().len())); + + result + } + + pub(crate) fn extend(self, rhs: Self) -> Self { + Self { + include: match (self.include, rhs.include) { + (None, None) => None, + (Some(lhs), None) => Some(lhs), + (None, Some(rhs)) => Some(rhs), + (Some(lhs), Some(rhs)) => { + Some(PathOrPatternSet(lhs.0.into_iter().filter(|p| rhs.0.contains(p)).collect())) + } + }, + exclude: PathOrPatternSet([self.exclude.0, rhs.exclude.0].concat()), + } + } +} + +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub struct PathOrPatternSet(Vec); + +impl PathOrPatternSet { + pub fn new(elements: Vec) -> Self { + Self(elements) + } + + pub fn from_absolute_paths(path: Vec) -> Result { + Ok(Self( + path + .into_iter() + .map(|p| PathOrPattern::new(&p)) + .collect::, _>>()?, + )) + } + + pub fn inner(&self) -> &Vec { + &self.0 + } + + pub fn into_path_or_patterns(self) -> Vec { + self.0 + } + + pub fn matches_path(&self, path: &Path) -> bool { + self.0.iter().any(|p| p.matches_path(path)) + } + + pub fn base_paths(&self) -> Vec { + let mut result = Vec::with_capacity(self.0.len()); + for element in &self.0 { + match element { + PathOrPattern::Path(path) => { + result.push(path.to_path_buf()); + } + PathOrPattern::RemoteUrl(_) => { + // ignore + } + PathOrPattern::Pattern(pattern) => { + result.push(pattern.base_path()); + } + } + } + result + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PathOrPattern { + Path(PathBuf), + RemoteUrl(Url), + Pattern(GlobPattern), +} + +impl PathOrPattern { + pub fn new(path: &str) -> Result { + if path.starts_with("http://") + || path.starts_with("https://") + || path.starts_with("file://") + { + let url = Url::parse(path)?; + if url.scheme() == "file" { + let path = url + .to_file_path() + .map_err(|_| anyhow::anyhow!("Invalid file URL: \"{}\"", path))?; + return Ok(Self::Path(path)); + } else { + return Ok(Self::RemoteUrl(url)); + } + } + + GlobPattern::new_if_pattern(path).map(|maybe_pattern| { + maybe_pattern + .map(PathOrPattern::Pattern) + .unwrap_or_else(|| PathOrPattern::Path(normalize_path(path))) + }) + } + + pub fn matches_path(&self, path: &Path) -> bool { + match self { + PathOrPattern::Path(p) => path.starts_with(p), + PathOrPattern::RemoteUrl(_) => false, + PathOrPattern::Pattern(p) => p.matches_path(path), + } + } + + /// Returns the base path of the pattern if it's not a remote url pattern. + pub fn base_path(&self) -> Option { + match self { + PathOrPattern::Path(p) => Some(p.clone()), + PathOrPattern::RemoteUrl(_) => None, + PathOrPattern::Pattern(p) => Some(p.base_path()), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct GlobPattern(glob::Pattern); + +impl GlobPattern { + pub fn new_if_pattern(pattern: &str) -> Result, anyhow::Error> { + if !is_glob_pattern(pattern) { + return Ok(None); + } + Self::new(pattern).map(Some) + } + + pub fn new(pattern: &str) -> Result { + let pattern = escape_brackets(pattern) + .replace('\\', "/") + .replace("/./", "/"); + let pattern = glob::Pattern::new(&pattern) + .with_context(|| format!("Failed to expand glob: \"{}\"", pattern))?; + Ok(Self(pattern)) + } + + pub fn matches_path(&self, path: &Path) -> bool { + self.0.matches_path_with(path, match_options()) + } + + pub fn base_path(&self) -> PathBuf { + let base_path = self + .0 + .as_str() + .split('/') + .take_while(|c| !has_glob_chars(c)) + .collect::>() + .join(std::path::MAIN_SEPARATOR_STR); + PathBuf::from(base_path) + } +} + +pub fn is_glob_pattern(path: &str) -> bool { + !path.starts_with("http:") + && !path.starts_with("https:") + && !path.starts_with("file:") + && has_glob_chars(path) +} + +fn has_glob_chars(pattern: &str) -> bool { + // we don't support [ and ] + pattern.chars().any(|c| matches!(c, '*' | '?')) +} + +fn escape_brackets(pattern: &str) -> String { + // Escape brackets - we currently don't support them, because with introduction + // of glob expansion paths like "pages/[id].ts" would suddenly start giving + // wrong results. We might want to revisit that in the future. + pattern.replace('[', "[[]").replace(']', "[]]") +} + +fn match_options() -> glob::MatchOptions { + // Matches what `deno_task_shell` does + glob::MatchOptions { + // false because it should work the same way on case insensitive file systems + case_sensitive: false, + // true because it copies what sh does + require_literal_separator: true, + // true because it copies with sh does—these files are considered "hidden" + require_literal_leading_dot: true, + } +} + +/// Normalize all intermediate components of the path (ie. remove "./" and "../" components). +/// Similar to `fs::canonicalize()` but doesn't resolve symlinks. +/// +/// Taken from Cargo +/// +fn normalize_path>(path: P) -> PathBuf { + let mut components = path.as_ref().components().peekable(); + let mut ret = + if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use tempfile::TempDir; + + use super::*; + + // For easier comparisons in tests. + #[derive(Debug, PartialEq, Eq)] + struct ComparableFilePatterns { + include: Option>, + exclude: Vec, + } + + impl ComparableFilePatterns { + pub fn new(root: &Path, file_patterns: &FilePatterns) -> Self { + fn path_or_pattern_to_string(root: &Path, p: &PathOrPattern) -> Option { + match p { + PathOrPattern::RemoteUrl(_) => None, + PathOrPattern::Path(p) => Some(p + .strip_prefix(root) + .unwrap() + .to_string_lossy() + .replace('\\', "/")), + PathOrPattern::Pattern(p) => Some(p + .0 + .as_str() + .strip_prefix(&format!( + "{}/", + root.to_string_lossy().replace('\\', "/") + )) + .unwrap() + .to_string()), + } + } + + Self { + include: file_patterns.include.as_ref().map(|p| { + p.0 + .iter() + .filter_map(|p| path_or_pattern_to_string(root, p)) + .collect() + }), + exclude: file_patterns + .exclude + .0 + .iter() + .filter_map(|p| path_or_pattern_to_string(root, p)) + .collect(), + } + } + + pub fn from_split( + root: &Path, + patterns_by_base: &[(PathBuf, FilePatterns)], + ) -> Vec<(String, ComparableFilePatterns)> { + patterns_by_base + .iter() + .map(|(base_path, file_patterns)| { + ( + base_path + .strip_prefix(root) + .unwrap() + .to_string_lossy() + .replace('\\', "/"), + ComparableFilePatterns::new(root, file_patterns), + ) + }) + .collect() + } + } + + #[test] + fn should_split_globs_by_base_dir() { + let temp_dir = TempDir::new().unwrap(); + let patterns = FilePatterns { + include: Some(PathOrPatternSet::new(vec![ + PathOrPattern::Pattern( + GlobPattern::new(&format!( + "{}/inner/**/*.ts", + temp_dir.path().to_string_lossy().replace('\\', "/") + )) + .unwrap(), + ), + PathOrPattern::Pattern( + GlobPattern::new(&format!( + "{}/inner/sub/deeper/**/*.js", + temp_dir.path().to_string_lossy().replace('\\', "/") + )) + .unwrap(), + ), + PathOrPattern::Pattern( + GlobPattern::new(&format!( + "{}/other/**/*.js", + temp_dir.path().to_string_lossy().replace('\\', "/") + )) + .unwrap(), + ), + PathOrPattern::Path(temp_dir.path().join("sub/file.ts").to_path_buf()), + ])), + exclude: PathOrPatternSet::new(vec![ + PathOrPattern::Pattern( + GlobPattern::new(&format!( + "{}/inner/other/**/*.ts", + temp_dir.path().to_string_lossy().replace('\\', "/") + )) + .unwrap(), + ), + PathOrPattern::Path( + temp_dir + .path() + .join("inner/sub/deeper/file.js") + .to_path_buf(), + ), + ]), + }; + let split = ComparableFilePatterns::from_split( + temp_dir.path(), + &patterns.split_by_base(), + ); + assert_eq!( + split, + vec![ + ( + "inner/sub/deeper".to_string(), + ComparableFilePatterns { + include: Some(vec![ + "inner/sub/deeper/**/*.js".to_string(), + "inner/**/*.ts".to_string(), + ]), + exclude: vec!["inner/sub/deeper/file.js".to_string()], + } + ), + ( + "sub/file.ts".to_string(), + ComparableFilePatterns { + include: Some(vec!["sub/file.ts".to_string()]), + exclude: vec![], + } + ), + ( + "inner".to_string(), + ComparableFilePatterns { + include: Some(vec!["inner/**/*.ts".to_string()]), + exclude: vec![ + "inner/other/**/*.ts".to_string(), + "inner/sub/deeper/file.js".to_string(), + ], + } + ), + ( + "other".to_string(), + ComparableFilePatterns { + include: Some(vec!["other/**/*.js".to_string()]), + exclude: vec![], + } + ) + ] + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7a8b775..e4d9d4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ use std::path::Path; use std::path::PathBuf; use url::Url; +pub mod glob; mod ts; mod util; @@ -28,6 +29,10 @@ pub use ts::IgnoredCompilerOptions; pub use ts::JsxImportSourceConfig; pub use ts::TsConfig; +use self::glob::FilePatterns; +use self::glob::PathOrPattern; +use self::glob::PathOrPatternSet; + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub enum ConfigFlag { #[default] @@ -55,19 +60,34 @@ impl SerializedFilesConfig { pub fn into_resolved( self, config_file_specifier: &Url, - ) -> Result { + ) -> Result { let config_dir = util::specifier_to_file_path(&util::specifier_parent( config_file_specifier, ))?; - Ok(FilesConfig { - include: self - .include - .map(|i| i.into_iter().map(|p| config_dir.join(p)).collect()), - exclude: self + let join_dir = |p: String| -> Result { + if glob::is_glob_pattern(&p) { + let p = p.strip_prefix("./").unwrap_or(&p); + let mut pattern = config_dir.to_string_lossy().replace('\\', "/"); + if !pattern.ends_with('/') { + pattern.push('/'); + } + let p = p.strip_suffix('/').unwrap_or(p); + pattern.push_str(p); + PathOrPattern::new(&pattern) + } else { + Ok(PathOrPattern::Path(config_dir.join(p))) + } + }; + Ok(FilePatterns { + include: match self.include { + Some(i) => Some(PathOrPatternSet::new(i.into_iter().map(join_dir).collect::, _>>()?)), + None => None, + }, + exclude: PathOrPatternSet::new(self .exclude .into_iter() - .map(|p| config_dir.join(p)) - .collect::>(), + .map(join_dir) + .collect::, _>>()?), }) } @@ -76,48 +96,6 @@ impl SerializedFilesConfig { } } -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct FilesConfig { - pub include: Option>, - pub exclude: Vec, -} - -impl FilesConfig { - /// Gets if the provided specifier is allowed based on the includes - /// and excludes in the configuration file. - pub fn matches_specifier(&self, specifier: &Url) -> bool { - let file_path = match util::specifier_to_file_path(specifier) { - Ok(file_path) => file_path, - Err(_) => return true, - }; - // Skip files which is in the exclude list. - if self.exclude.iter().any(|i| file_path.starts_with(i)) { - return false; - } - - // Ignore files not in the include list if it's present. - if let Some(include) = &self.include { - return include.iter().any(|p| file_path.starts_with(p)); - } - - true - } - - fn extend(self, rhs: Self) -> Self { - Self { - include: match (self.include, rhs.include) { - (None, None) => None, - (Some(lhs), None) => Some(lhs), - (None, Some(rhs)) => Some(rhs), - (Some(lhs), Some(rhs)) => { - Some(lhs.into_iter().filter(|p| rhs.contains(p)).collect()) - } - }, - exclude: [self.exclude, rhs.exclude].concat(), - } - } -} - /// Choose between flat and nested files configuration. /// /// `files` has precedence over `deprecated_files`. @@ -192,12 +170,12 @@ impl SerializedLintConfig { #[derive(Clone, Debug, Default, PartialEq)] pub struct LintConfig { pub rules: LintRulesConfig, - pub files: FilesConfig, + pub files: FilePatterns, pub report: Option, } impl LintConfig { - pub fn with_files(self, files: FilesConfig) -> Self { + pub fn with_files(self, files: FilePatterns) -> Self { let files = self.files.extend(files); Self { files, ..self } } @@ -320,11 +298,11 @@ impl SerializedFmtConfig { #[derive(Clone, Debug, Default, PartialEq)] pub struct FmtConfig { pub options: FmtOptionsConfig, - pub files: FilesConfig, + pub files: FilePatterns, } impl FmtConfig { - pub fn with_files(self, files: FilesConfig) -> Self { + pub fn with_files(self, files: FilePatterns) -> Self { let files = self.files.extend(files); Self { files, ..self } } @@ -385,11 +363,11 @@ impl SerializedTestConfig { #[derive(Clone, Debug, Default, PartialEq)] pub struct TestConfig { - pub files: FilesConfig, + pub files: FilePatterns, } impl TestConfig { - pub fn with_files(self, files: FilesConfig) -> Self { + pub fn with_files(self, files: FilePatterns) -> Self { let files = self.files.extend(files); Self { files } } @@ -440,11 +418,11 @@ pub struct WorkspaceMemberConfig { #[derive(Clone, Debug, Default, PartialEq)] pub struct BenchConfig { - pub files: FilesConfig, + pub files: FilePatterns, } impl BenchConfig { - pub fn with_files(self, files: FilesConfig) -> Self { + pub fn with_files(self, files: FilePatterns) -> Self { let files = self.files.extend(files); Self { files } } @@ -773,7 +751,7 @@ impl ConfigFile { }) } - pub fn to_files_config(&self) -> Result, AnyError> { + pub fn to_files_config(&self) -> Result, AnyError> { let mut exclude: Vec = if let Some(exclude) = self.json.exclude.clone() { serde_json::from_value(exclude) @@ -1313,9 +1291,9 @@ mod tests { assert_eq!( unpack_object(config_file.to_lint_config(), "lint"), LintConfig { - files: FilesConfig { - include: Some(vec![PathBuf::from("/deno/src/")]), - exclude: vec![PathBuf::from("/deno/src/testdata/")], + files: FilePatterns { + include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/"))])), + exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/testdata/"))]), }, rules: LintRulesConfig { include: Some(vec!["ban-untagged-todo".to_string()]), @@ -1328,9 +1306,9 @@ mod tests { assert_eq!( unpack_object(config_file.to_fmt_config(), "fmt"), FmtConfig { - files: FilesConfig { - include: Some(vec![PathBuf::from("/deno/src/")]), - exclude: vec![PathBuf::from("/deno/src/testdata/")], + files: FilePatterns { + include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/"))])), + exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/testdata/"))]), }, options: FmtOptionsConfig { use_tabs: Some(true), @@ -1387,30 +1365,30 @@ mod tests { let lint_files = unpack_object(config_file.to_lint_config(), "lint").files; assert_eq!( lint_files, - FilesConfig { - include: Some(vec![PathBuf::from("/deno/src/")]), - exclude: vec![], + FilePatterns { + include: Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap()), + exclude: Default::default(), } ); let fmt_files = unpack_object(config_file.to_fmt_config(), "fmt").files; assert_eq!( fmt_files, - FilesConfig { - exclude: vec![PathBuf::from("/deno/dist/")], + FilePatterns { include: None, + exclude: PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]).unwrap(), } ); let test_include = unpack_object(config_file.to_test_config(), "test") .files .include; - assert_eq!(test_include, Some(vec![PathBuf::from("/deno/src/")])); + assert_eq!(test_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); let bench_include = unpack_object(config_file.to_bench_config(), "bench") .files .include; - assert_eq!(bench_include, Some(vec![PathBuf::from("/deno/src/")])); + assert_eq!(bench_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); } #[test] @@ -1428,22 +1406,23 @@ mod tests { let lint_include = unpack_object(config_file.to_lint_config(), "lint") .files .include; - assert_eq!(lint_include, Some(vec![PathBuf::from("/deno/src/")])); + assert_eq!(lint_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); let fmt_include = unpack_object(config_file.to_fmt_config(), "fmt") .files .include; - assert_eq!(fmt_include, Some(vec![PathBuf::from("/deno/src/")])); + assert_eq!(fmt_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); let test_exclude = unpack_object(config_file.to_test_config(), "test") .files .exclude; - assert_eq!(test_exclude, vec![PathBuf::from("/deno/dist/")]); + assert_eq!(test_exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]).unwrap()); let bench_exclude = unpack_object(config_file.to_bench_config(), "bench") .files .exclude; - assert_eq!(bench_exclude, vec![PathBuf::from("/deno/dist/")]); + assert_eq!(bench_exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]).unwrap()); + } #[test] @@ -1515,16 +1494,10 @@ mod tests { let test_config = config_file.to_test_config().unwrap().unwrap(); assert_eq!(test_config.files.include, None); - assert_eq!( - test_config.files.exclude, - vec![PathBuf::from("/deno/npm/"), PathBuf::from("/deno/foo/")] - ); + assert_eq!(test_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string(), "/deno/foo/".to_string()]).unwrap()); let bench_config = config_file.to_bench_config().unwrap().unwrap(); - assert_eq!( - bench_config.files.exclude, - vec![PathBuf::from("/deno/foo/")] - ); + assert_eq!(bench_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/foo/".to_string()]).unwrap()); } #[test] @@ -1540,15 +1513,15 @@ mod tests { let files_config = config_file.to_files_config().unwrap().unwrap(); assert_eq!(files_config.include, None); - assert_eq!(files_config.exclude, vec![PathBuf::from("/deno/npm/")]); + assert_eq!(files_config.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]).unwrap()); let lint_config = config_file.to_lint_config().unwrap().unwrap(); assert_eq!(lint_config.files.include, None); - assert_eq!(lint_config.files.exclude, vec![PathBuf::from("/deno/npm/")]); + assert_eq!(lint_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]).unwrap()); let fmt_config = config_file.to_fmt_config().unwrap().unwrap(); assert_eq!(fmt_config.files.include, None); - assert_eq!(fmt_config.files.exclude, vec![PathBuf::from("/deno/npm/")],); + assert_eq!(fmt_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]).unwrap()); } #[test] @@ -1634,7 +1607,7 @@ mod tests { .unwrap() .to_file_path() .unwrap(); - assert_eq!(fmt_config.files.exclude, vec![expected_exclude]); + assert_eq!(fmt_config.files.exclude, PathOrPatternSet::new(vec![PathOrPattern::Path(expected_exclude)])); // Now add all ancestors of testdata to checked. for a in testdata.as_path().ancestors() { @@ -1713,8 +1686,8 @@ mod tests { } #[test] - fn files_config_matches_remote() { - assert!(FilesConfig::default() + fn files_pattern_matches_remote() { + assert!(FilePatterns::default() .matches_specifier(&Url::parse("https://example.com/mod.ts").unwrap())); } From 484b108cc89b8afe815b3e662738a34beedd172d Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 12 Jan 2024 20:40:33 -0500 Subject: [PATCH 2/8] Format --- src/glob.rs | 45 ++++++++++--------- src/lib.rs | 125 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 125 insertions(+), 45 deletions(-) diff --git a/src/glob.rs b/src/glob.rs index 65139be..7e640e1 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -5,8 +5,8 @@ use std::path::Path; use std::path::PathBuf; use anyhow::Context; -use url::Url; use indexmap::IndexMap; +use url::Url; #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct FilePatterns { @@ -56,7 +56,7 @@ impl FilePatterns { match path_or_pattern { PathOrPattern::Path(path) => include_paths.push((path.is_file(), path)), PathOrPattern::Pattern(pattern) => include_patterns.push(pattern), - PathOrPattern::RemoteUrl(_) => {}, + PathOrPattern::RemoteUrl(_) => {} } } let include_patterns_by_base_path = include_patterns.into_iter().fold( @@ -159,9 +159,9 @@ impl FilePatterns { (None, None) => None, (Some(lhs), None) => Some(lhs), (None, Some(rhs)) => Some(rhs), - (Some(lhs), Some(rhs)) => { - Some(PathOrPatternSet(lhs.0.into_iter().filter(|p| rhs.0.contains(p)).collect())) - } + (Some(lhs), Some(rhs)) => Some(PathOrPatternSet( + lhs.0.into_iter().filter(|p| rhs.0.contains(p)).collect(), + )), }, exclude: PathOrPatternSet([self.exclude.0, rhs.exclude.0].concat()), } @@ -381,23 +381,28 @@ mod test { impl ComparableFilePatterns { pub fn new(root: &Path, file_patterns: &FilePatterns) -> Self { - fn path_or_pattern_to_string(root: &Path, p: &PathOrPattern) -> Option { + fn path_or_pattern_to_string( + root: &Path, + p: &PathOrPattern, + ) -> Option { match p { PathOrPattern::RemoteUrl(_) => None, - PathOrPattern::Path(p) => Some(p - .strip_prefix(root) - .unwrap() - .to_string_lossy() - .replace('\\', "/")), - PathOrPattern::Pattern(p) => Some(p - .0 - .as_str() - .strip_prefix(&format!( - "{}/", - root.to_string_lossy().replace('\\', "/") - )) - .unwrap() - .to_string()), + PathOrPattern::Path(p) => Some( + p.strip_prefix(root) + .unwrap() + .to_string_lossy() + .replace('\\', "/"), + ), + PathOrPattern::Pattern(p) => Some( + p.0 + .as_str() + .strip_prefix(&format!( + "{}/", + root.to_string_lossy().replace('\\', "/") + )) + .unwrap() + .to_string(), + ), } } diff --git a/src/lib.rs b/src/lib.rs index e4d9d4c..6f2f11b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,14 +80,18 @@ impl SerializedFilesConfig { }; Ok(FilePatterns { include: match self.include { - Some(i) => Some(PathOrPatternSet::new(i.into_iter().map(join_dir).collect::, _>>()?)), + Some(i) => Some(PathOrPatternSet::new( + i.into_iter().map(join_dir).collect::, _>>()?, + )), None => None, }, - exclude: PathOrPatternSet::new(self - .exclude - .into_iter() - .map(join_dir) - .collect::, _>>()?), + exclude: PathOrPatternSet::new( + self + .exclude + .into_iter() + .map(join_dir) + .collect::, _>>()?, + ), }) } @@ -1292,8 +1296,12 @@ mod tests { unpack_object(config_file.to_lint_config(), "lint"), LintConfig { files: FilePatterns { - include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/"))])), - exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/testdata/"))]), + include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path( + PathBuf::from("/deno/src/") + )])), + exclude: PathOrPatternSet::new(vec![PathOrPattern::Path( + PathBuf::from("/deno/src/testdata/") + )]), }, rules: LintRulesConfig { include: Some(vec!["ban-untagged-todo".to_string()]), @@ -1307,8 +1315,12 @@ mod tests { unpack_object(config_file.to_fmt_config(), "fmt"), FmtConfig { files: FilePatterns { - include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/"))])), - exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(PathBuf::from("/deno/src/testdata/"))]), + include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path( + PathBuf::from("/deno/src/") + )])), + exclude: PathOrPatternSet::new(vec![PathOrPattern::Path( + PathBuf::from("/deno/src/testdata/") + )]), }, options: FmtOptionsConfig { use_tabs: Some(true), @@ -1366,7 +1378,10 @@ mod tests { assert_eq!( lint_files, FilePatterns { - include: Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap()), + include: Some( + PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + .unwrap() + ), exclude: Default::default(), } ); @@ -1376,19 +1391,34 @@ mod tests { fmt_files, FilePatterns { include: None, - exclude: PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]).unwrap(), + exclude: PathOrPatternSet::from_absolute_paths(vec![ + "/deno/dist/".to_string() + ]) + .unwrap(), } ); let test_include = unpack_object(config_file.to_test_config(), "test") .files .include; - assert_eq!(test_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); + assert_eq!( + test_include, + Some( + PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + .unwrap() + ) + ); let bench_include = unpack_object(config_file.to_bench_config(), "bench") .files .include; - assert_eq!(bench_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); + assert_eq!( + bench_include, + Some( + PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + .unwrap() + ) + ); } #[test] @@ -1406,23 +1436,42 @@ mod tests { let lint_include = unpack_object(config_file.to_lint_config(), "lint") .files .include; - assert_eq!(lint_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); + assert_eq!( + lint_include, + Some( + PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + .unwrap() + ) + ); let fmt_include = unpack_object(config_file.to_fmt_config(), "fmt") .files .include; - assert_eq!(fmt_include, Some(PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]).unwrap())); + assert_eq!( + fmt_include, + Some( + PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + .unwrap() + ) + ); let test_exclude = unpack_object(config_file.to_test_config(), "test") .files .exclude; - assert_eq!(test_exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]).unwrap()); + assert_eq!( + test_exclude, + PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]) + .unwrap() + ); let bench_exclude = unpack_object(config_file.to_bench_config(), "bench") .files .exclude; - assert_eq!(bench_exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]).unwrap()); - + assert_eq!( + bench_exclude, + PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]) + .unwrap() + ); } #[test] @@ -1494,10 +1543,21 @@ mod tests { let test_config = config_file.to_test_config().unwrap().unwrap(); assert_eq!(test_config.files.include, None); - assert_eq!(test_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string(), "/deno/foo/".to_string()]).unwrap()); + assert_eq!( + test_config.files.exclude, + PathOrPatternSet::from_absolute_paths(vec![ + "/deno/npm/".to_string(), + "/deno/foo/".to_string() + ]) + .unwrap() + ); let bench_config = config_file.to_bench_config().unwrap().unwrap(); - assert_eq!(bench_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/foo/".to_string()]).unwrap()); + assert_eq!( + bench_config.files.exclude, + PathOrPatternSet::from_absolute_paths(vec!["/deno/foo/".to_string()]) + .unwrap() + ); } #[test] @@ -1513,15 +1573,27 @@ mod tests { let files_config = config_file.to_files_config().unwrap().unwrap(); assert_eq!(files_config.include, None); - assert_eq!(files_config.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]).unwrap()); + assert_eq!( + files_config.exclude, + PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]) + .unwrap() + ); let lint_config = config_file.to_lint_config().unwrap().unwrap(); assert_eq!(lint_config.files.include, None); - assert_eq!(lint_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]).unwrap()); + assert_eq!( + lint_config.files.exclude, + PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]) + .unwrap() + ); let fmt_config = config_file.to_fmt_config().unwrap().unwrap(); assert_eq!(fmt_config.files.include, None); - assert_eq!(fmt_config.files.exclude, PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]).unwrap()); + assert_eq!( + fmt_config.files.exclude, + PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]) + .unwrap() + ); } #[test] @@ -1607,7 +1679,10 @@ mod tests { .unwrap() .to_file_path() .unwrap(); - assert_eq!(fmt_config.files.exclude, PathOrPatternSet::new(vec![PathOrPattern::Path(expected_exclude)])); + assert_eq!( + fmt_config.files.exclude, + PathOrPatternSet::new(vec![PathOrPattern::Path(expected_exclude)]) + ); // Now add all ancestors of testdata to checked. for a in testdata.as_path().ancestors() { From 13429bcd9688752ad48022589208dd7e2849de61 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 12 Jan 2024 21:35:20 -0500 Subject: [PATCH 3/8] More improvements --- .gitignore | 1 + src/glob.rs | 62 ++++++++++++++++++++++++++++++++++++++++++----------- src/lib.rs | 61 ++++++++++++++++++++-------------------------------- 3 files changed, 74 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..ccb5166 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.vscode \ No newline at end of file diff --git a/src/glob.rs b/src/glob.rs index 7e640e1..7b93d13 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -176,11 +176,23 @@ impl PathOrPatternSet { Self(elements) } - pub fn from_absolute_paths(path: Vec) -> Result { + pub fn from_absolute_paths(paths: &[String]) -> Result { Ok(Self( - path - .into_iter() - .map(|p| PathOrPattern::new(&p)) + paths + .iter() + .map(|p| PathOrPattern::new(p)) + .collect::, _>>()?, + )) + } + + pub fn from_relative_path_or_patterns( + base: &Path, + paths: &[String], + ) -> Result { + Ok(Self( + paths + .iter() + .map(|p| PathOrPattern::from_relative(base, p)) .collect::, _>>()?, )) } @@ -216,7 +228,7 @@ impl PathOrPatternSet { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub enum PathOrPattern { Path(PathBuf), RemoteUrl(Url), @@ -247,6 +259,24 @@ impl PathOrPattern { }) } + pub fn from_relative( + base: &Path, + p: &str, + ) -> Result { + if is_glob_pattern(p) { + let p = p.strip_prefix("./").unwrap_or(p); + let mut pattern = base.to_string_lossy().replace('\\', "/"); + if !pattern.ends_with('/') { + pattern.push('/'); + } + let p = p.strip_suffix('/').unwrap_or(p); + pattern.push_str(p); + PathOrPattern::new(&pattern) + } else { + Ok(PathOrPattern::Path(base.join(p))) + } + } + pub fn matches_path(&self, path: &Path) -> bool { match self { PathOrPattern::Path(p) => path.starts_with(p), @@ -265,7 +295,7 @@ impl PathOrPattern { } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct GlobPattern(glob::Pattern); impl GlobPattern { @@ -277,9 +307,7 @@ impl GlobPattern { } pub fn new(pattern: &str) -> Result { - let pattern = escape_brackets(pattern) - .replace('\\', "/") - .replace("/./", "/"); + let pattern = escape_brackets(pattern).replace('\\', "/"); let pattern = glob::Pattern::new(&pattern) .with_context(|| format!("Failed to expand glob: \"{}\"", pattern))?; Ok(Self(pattern)) @@ -302,9 +330,9 @@ impl GlobPattern { } pub fn is_glob_pattern(path: &str) -> bool { - !path.starts_with("http:") - && !path.starts_with("https:") - && !path.starts_with("file:") + !path.starts_with("http://") + && !path.starts_with("https://") + && !path.starts_with("file://") && has_glob_chars(path) } @@ -530,4 +558,14 @@ mod test { ] ); } + + #[test] + fn from_relative() { + let cwd = std::env::current_dir().unwrap(); + let pattern = PathOrPattern::from_relative(&cwd, "./**/*.ts").unwrap(); + assert_eq!(pattern.matches_path(&cwd.join("foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("foo.js")), false); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.js")), false); + } } diff --git a/src/lib.rs b/src/lib.rs index 6f2f11b..0f74d5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,6 @@ pub use ts::JsxImportSourceConfig; pub use ts::TsConfig; use self::glob::FilePatterns; -use self::glob::PathOrPattern; use self::glob::PathOrPatternSet; #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -64,34 +63,18 @@ impl SerializedFilesConfig { let config_dir = util::specifier_to_file_path(&util::specifier_parent( config_file_specifier, ))?; - let join_dir = |p: String| -> Result { - if glob::is_glob_pattern(&p) { - let p = p.strip_prefix("./").unwrap_or(&p); - let mut pattern = config_dir.to_string_lossy().replace('\\', "/"); - if !pattern.ends_with('/') { - pattern.push('/'); - } - let p = p.strip_suffix('/').unwrap_or(p); - pattern.push_str(p); - PathOrPattern::new(&pattern) - } else { - Ok(PathOrPattern::Path(config_dir.join(p))) - } - }; Ok(FilePatterns { include: match self.include { - Some(i) => Some(PathOrPatternSet::new( - i.into_iter().map(join_dir).collect::, _>>()?, - )), + Some(i) => Some(PathOrPatternSet::from_relative_path_or_patterns( + &config_dir, + &i, + )?), None => None, }, - exclude: PathOrPatternSet::new( - self - .exclude - .into_iter() - .map(join_dir) - .collect::, _>>()?, - ), + exclude: PathOrPatternSet::from_relative_path_or_patterns( + &config_dir, + &self.exclude, + )?, }) } @@ -1206,6 +1189,8 @@ pub fn get_ts_config_for_emit( #[cfg(test)] mod tests { + use crate::glob::PathOrPattern; + use super::*; use pretty_assertions::assert_eq; use std::path::PathBuf; @@ -1379,7 +1364,7 @@ mod tests { lint_files, FilePatterns { include: Some( - PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/src/".to_string()]) .unwrap() ), exclude: Default::default(), @@ -1391,7 +1376,7 @@ mod tests { fmt_files, FilePatterns { include: None, - exclude: PathOrPatternSet::from_absolute_paths(vec![ + exclude: PathOrPatternSet::from_absolute_paths(&[ "/deno/dist/".to_string() ]) .unwrap(), @@ -1404,7 +1389,7 @@ mod tests { assert_eq!( test_include, Some( - PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/src/".to_string()]) .unwrap() ) ); @@ -1415,7 +1400,7 @@ mod tests { assert_eq!( bench_include, Some( - PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/src/".to_string()]) .unwrap() ) ); @@ -1439,7 +1424,7 @@ mod tests { assert_eq!( lint_include, Some( - PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/src/".to_string()]) .unwrap() ) ); @@ -1450,7 +1435,7 @@ mod tests { assert_eq!( fmt_include, Some( - PathOrPatternSet::from_absolute_paths(vec!["/deno/src/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/src/".to_string()]) .unwrap() ) ); @@ -1460,7 +1445,7 @@ mod tests { .exclude; assert_eq!( test_exclude, - PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/dist/".to_string()]) .unwrap() ); @@ -1469,7 +1454,7 @@ mod tests { .exclude; assert_eq!( bench_exclude, - PathOrPatternSet::from_absolute_paths(vec!["/deno/dist/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/dist/".to_string()]) .unwrap() ); } @@ -1545,7 +1530,7 @@ mod tests { assert_eq!(test_config.files.include, None); assert_eq!( test_config.files.exclude, - PathOrPatternSet::from_absolute_paths(vec![ + PathOrPatternSet::from_absolute_paths(&[ "/deno/npm/".to_string(), "/deno/foo/".to_string() ]) @@ -1555,7 +1540,7 @@ mod tests { let bench_config = config_file.to_bench_config().unwrap().unwrap(); assert_eq!( bench_config.files.exclude, - PathOrPatternSet::from_absolute_paths(vec!["/deno/foo/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/foo/".to_string()]) .unwrap() ); } @@ -1575,7 +1560,7 @@ mod tests { assert_eq!(files_config.include, None); assert_eq!( files_config.exclude, - PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/npm/".to_string()]) .unwrap() ); @@ -1583,7 +1568,7 @@ mod tests { assert_eq!(lint_config.files.include, None); assert_eq!( lint_config.files.exclude, - PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/npm/".to_string()]) .unwrap() ); @@ -1591,7 +1576,7 @@ mod tests { assert_eq!(fmt_config.files.include, None); assert_eq!( fmt_config.files.exclude, - PathOrPatternSet::from_absolute_paths(vec!["/deno/npm/".to_string()]) + PathOrPatternSet::from_absolute_paths(&["/deno/npm/".to_string()]) .unwrap() ); } From b4b1835c08670e1b05f7907af3e732c361fc8562 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 12 Jan 2024 22:20:31 -0500 Subject: [PATCH 4/8] Add test --- src/glob.rs | 14 ++++++++++++++ src/lib.rs | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/glob.rs b/src/glob.rs index 7b93d13..d7f6131 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -313,6 +313,10 @@ impl GlobPattern { Ok(Self(pattern)) } + pub fn as_str(&self) -> &str { + self.0.as_str() + } + pub fn matches_path(&self, path: &Path) -> bool { self.0.matches_path_with(path, match_options()) } @@ -568,4 +572,14 @@ mod test { assert_eq!(pattern.matches_path(&cwd.join("foo.js")), false); assert_eq!(pattern.matches_path(&cwd.join("dir/foo.js")), false); } + + #[test] + fn from_relative_dot_slash() { + let cwd = std::env::current_dir().unwrap(); + let pattern = PathOrPattern::from_relative(&cwd, "./").unwrap(); + match pattern { + PathOrPattern::Path(p) => assert_eq!(p, cwd), + PathOrPattern::RemoteUrl(_) | PathOrPattern::Pattern(_) => unreachable!(), + } + } } diff --git a/src/lib.rs b/src/lib.rs index 0f74d5a..3fe927e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,13 +68,13 @@ impl SerializedFilesConfig { Some(i) => Some(PathOrPatternSet::from_relative_path_or_patterns( &config_dir, &i, - )?), + ).context("Invalid config file include.")?), None => None, }, exclude: PathOrPatternSet::from_relative_path_or_patterns( &config_dir, &self.exclude, - )?, + ).context("Invalid config file exclude.")?, }) } From 4570310d3b9bbc67a45526b72b9180ac78dd5b2b Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 12 Jan 2024 22:29:48 -0500 Subject: [PATCH 5/8] Fix bug with specifiers --- src/glob.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/glob.rs b/src/glob.rs index d7f6131..b267e0a 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -272,6 +272,11 @@ impl PathOrPattern { let p = p.strip_suffix('/').unwrap_or(p); pattern.push_str(p); PathOrPattern::new(&pattern) + } else if p.starts_with("http://") + || p.starts_with("https://") + || p.starts_with("file://") + { + PathOrPattern::new(p) } else { Ok(PathOrPattern::Path(base.join(p))) } @@ -582,4 +587,32 @@ mod test { PathOrPattern::RemoteUrl(_) | PathOrPattern::Pattern(_) => unreachable!(), } } + + #[test] + fn from_relative_specifier() { + let cwd = std::env::current_dir().unwrap(); + for scheme in &["http", "https"] { + let url = format!("{}://deno.land/x/test", scheme); + let pattern = PathOrPattern::from_relative(&cwd, &url).unwrap(); + match pattern { + PathOrPattern::RemoteUrl(p) => { + assert_eq!(p.as_str(), url) + } + PathOrPattern::Path(_) | PathOrPattern::Pattern(_) => unreachable!(), + } + } + { + let file_specifier = Url::from_directory_path(&cwd).unwrap(); + let pattern = + PathOrPattern::from_relative(&cwd, file_specifier.as_str()).unwrap(); + match pattern { + PathOrPattern::Path(p) => { + assert_eq!(p, cwd); + } + PathOrPattern::RemoteUrl(_) | PathOrPattern::Pattern(_) => { + unreachable!() + } + } + } + } } From bf3557c716ac187e71098ff17cda3742468beffc Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 12 Jan 2024 22:31:29 -0500 Subject: [PATCH 6/8] Format --- src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3fe927e..b99adac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,16 +65,17 @@ impl SerializedFilesConfig { ))?; Ok(FilePatterns { include: match self.include { - Some(i) => Some(PathOrPatternSet::from_relative_path_or_patterns( - &config_dir, - &i, - ).context("Invalid config file include.")?), + Some(i) => Some( + PathOrPatternSet::from_relative_path_or_patterns(&config_dir, &i) + .context("Invalid config file include.")?, + ), None => None, }, exclude: PathOrPatternSet::from_relative_path_or_patterns( &config_dir, &self.exclude, - ).context("Invalid config file exclude.")?, + ) + .context("Invalid config file exclude.")?, }) } From 94959306c8ba3de20953e12b68b6a2c92f77013b Mon Sep 17 00:00:00 2001 From: David Sherret Date: Sat, 13 Jan 2024 10:57:37 -0500 Subject: [PATCH 7/8] More tests --- src/glob.rs | 74 ++++++++++++++++++++++++----------------------------- src/util.rs | 40 +++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/src/glob.rs b/src/glob.rs index b267e0a..32f330e 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use std::path::Component; use std::path::Path; use std::path::PathBuf; @@ -8,6 +7,9 @@ use anyhow::Context; use indexmap::IndexMap; use url::Url; +use crate::util::normalize_path; +use crate::util::specifier_to_file_path; + #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct FilePatterns { pub include: Option, @@ -19,7 +21,7 @@ impl FilePatterns { if specifier.scheme() != "file" { return true; } - let path = match specifier.to_file_path() { + let path = match specifier_to_file_path(specifier) { Ok(path) => path, Err(_) => return true, }; @@ -369,39 +371,6 @@ fn match_options() -> glob::MatchOptions { } } -/// Normalize all intermediate components of the path (ie. remove "./" and "../" components). -/// Similar to `fs::canonicalize()` but doesn't resolve symlinks. -/// -/// Taken from Cargo -/// -fn normalize_path>(path: P) -> PathBuf { - let mut components = path.as_ref().components().peekable(); - let mut ret = - if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { - components.next(); - PathBuf::from(c.as_os_str()) - } else { - PathBuf::new() - }; - - for component in components { - match component { - Component::Prefix(..) => unreachable!(), - Component::RootDir => { - ret.push(component.as_os_str()); - } - Component::CurDir => {} - Component::ParentDir => { - ret.pop(); - } - Component::Normal(c) => { - ret.push(c); - } - } - } - ret -} - #[cfg(test)] mod test { use pretty_assertions::assert_eq; @@ -571,11 +540,36 @@ mod test { #[test] fn from_relative() { let cwd = std::env::current_dir().unwrap(); - let pattern = PathOrPattern::from_relative(&cwd, "./**/*.ts").unwrap(); - assert_eq!(pattern.matches_path(&cwd.join("foo.ts")), true); - assert_eq!(pattern.matches_path(&cwd.join("dir/foo.ts")), true); - assert_eq!(pattern.matches_path(&cwd.join("foo.js")), false); - assert_eq!(pattern.matches_path(&cwd.join("dir/foo.js")), false); + // leading dot slash + { + let pattern = PathOrPattern::from_relative(&cwd, "./**/*.ts").unwrap(); + assert_eq!(pattern.matches_path(&cwd.join("foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("foo.js")), false); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.js")), false); + } + // no leading dot slash + { + let pattern = PathOrPattern::from_relative(&cwd, "**/*.ts").unwrap(); + assert_eq!(pattern.matches_path(&cwd.join("foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("foo.js")), false); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.js")), false); + } + // exact file, leading dot slash + { + let pattern = PathOrPattern::from_relative(&cwd, "./foo.ts").unwrap(); + assert_eq!(pattern.matches_path(&cwd.join("foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.ts")), false); + assert_eq!(pattern.matches_path(&cwd.join("foo.js")), false); + } + // exact file, no leading dot slash + { + let pattern = PathOrPattern::from_relative(&cwd, "foo.ts").unwrap(); + assert_eq!(pattern.matches_path(&cwd.join("foo.ts")), true); + assert_eq!(pattern.matches_path(&cwd.join("dir/foo.ts")), false); + assert_eq!(pattern.matches_path(&cwd.join("foo.js")), false); + } } #[test] diff --git a/src/util.rs b/src/util.rs index b994d56..d65bd4b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -3,6 +3,8 @@ use anyhow::bail; use anyhow::Error as AnyError; use serde_json::Value; +use std::path::Component; +use std::path::Path; use std::path::PathBuf; use url::Url; @@ -64,6 +66,44 @@ pub fn specifier_to_file_path(specifier: &Url) -> Result { } } +/// Normalize all intermediate components of the path (ie. remove "./" and "../" components). +/// Similar to `fs::canonicalize()` but doesn't resolve symlinks. +/// +/// Taken from Cargo +/// +#[inline] +pub fn normalize_path>(path: P) -> PathBuf { + fn inner(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = + if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret + } + + inner(path.as_ref()) +} + /// A function that works like JavaScript's `Object.assign()`. pub fn json_merge(a: &mut Value, b: &Value) { match (a, b) { From d5a09308fb94db0b3f56c7475cbc4812b913c6ed Mon Sep 17 00:00:00 2001 From: David Sherret Date: Sat, 13 Jan 2024 10:58:00 -0500 Subject: [PATCH 8/8] Newline --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ccb5166..9026c77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /target -.vscode \ No newline at end of file +.vscode