use std::collections::VecDeque;
use std::path::Path;
use std::path::PathBuf;
use crate::environment::CanonicalizedPathBuf;
use super::is_negated_glob;
use super::is_pattern;
use super::non_negated_glob;
#[derive(Debug)]
pub struct GlobPatterns {
pub arg_includes: Option<Vec<GlobPattern>>,
pub config_includes: Option<Vec<GlobPattern>>,
pub arg_excludes: Option<Vec<GlobPattern>>,
pub config_excludes: Vec<GlobPattern>,
}
impl GlobPatterns {
pub fn include_paths(&self) -> Vec<PathBuf> {
self
.arg_includes
.iter()
.flat_map(|i| i.iter())
.chain(self.config_includes.iter().flat_map(|i| i.iter()))
.filter_map(|pattern| {
if !is_pattern(&pattern.relative_pattern) {
Some(pattern.base_dir.join(&pattern.relative_pattern))
} else {
None
}
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobPattern {
pub relative_pattern: String,
pub base_dir: CanonicalizedPathBuf,
}
impl GlobPattern {
pub fn new(relative_pattern: String, base_dir: CanonicalizedPathBuf) -> Self {
GlobPattern { relative_pattern, base_dir }
}
pub fn new_vec(relative_patterns: Vec<String>, base_dir: CanonicalizedPathBuf) -> Vec<Self> {
relative_patterns
.into_iter()
.map(|relative_pattern| GlobPattern::new(relative_pattern, base_dir.clone()))
.collect()
}
pub fn matches_dir_for_traversal(&self, dir_path: &Path) -> bool {
if self.is_negated() {
return false;
}
if self.base_dir.as_ref().starts_with(dir_path) {
true
} else if let Ok(remaining) = dir_path.strip_prefix(&self.base_dir) {
let pattern = self.relative_pattern.strip_prefix("./").unwrap_or(&self.relative_pattern);
let mut components = remaining.components().peekable();
let mut parts = pattern.split('/').peekable();
while let (Some(component), Some(part)) = (components.peek(), parts.peek()) {
let part = *part;
let component = *component;
if part == "**" {
return true;
} else if !is_pattern(part) && !component.as_os_str().eq_ignore_ascii_case(part) {
return false;
}
components.next();
parts.next();
}
parts.next().is_some()
} else {
false
}
}
pub fn is_negated(&self) -> bool {
is_negated_glob(&self.relative_pattern)
}
pub fn invert(self) -> Self {
if self.is_negated() {
GlobPattern {
base_dir: self.base_dir,
relative_pattern: non_negated_glob(&self.relative_pattern).to_string(),
}
} else {
GlobPattern {
base_dir: self.base_dir,
relative_pattern: format!("!{}", self.relative_pattern),
}
}
}
pub fn into_deepest_base(self) -> Self {
let is_negated = self.is_negated();
let pattern = non_negated_glob(&self.relative_pattern);
let stripped_dot_slash = pattern.starts_with("./");
let pattern = pattern.strip_prefix("./").unwrap_or(pattern);
let parts: Vec<&str> = pattern.split('/').collect();
let mut base_parts = Vec::new();
let mut remaining_parts = Vec::new();
let mut found_glob = false;
for part in &parts {
if !found_glob && !is_pattern(part) {
base_parts.push(*part);
} else {
found_glob = true;
remaining_parts.push(*part);
}
}
if !found_glob && !base_parts.is_empty() {
remaining_parts.push(base_parts.pop().unwrap());
}
let new_base_dir = if base_parts.is_empty() {
self.base_dir.clone()
} else {
self.base_dir.join_panic_relative(base_parts.join("/"))
};
let new_relative = remaining_parts.join("/");
let new_relative = if stripped_dot_slash { format!("./{}", new_relative) } else { new_relative };
let new_pattern = if is_negated { format!("!{}", new_relative) } else { new_relative };
GlobPattern {
base_dir: new_base_dir,
relative_pattern: new_pattern,
}
}
pub fn into_new_base(self, new_base_dir: CanonicalizedPathBuf) -> Option<Self> {
if self.base_dir == new_base_dir {
Some(self)
} else if let Ok(prefix) = self.base_dir.strip_prefix(&new_base_dir) {
let is_negated = self.is_negated();
let start_pattern = {
let mut value = prefix.to_string_lossy().to_string().replace('\\', "/");
if value.starts_with("./") {
value.drain(..2);
}
if value.starts_with('/') {
value.drain(..1);
}
value
};
let new_relative_pattern = {
let mut value = self.relative_pattern;
if is_negated {
value.drain(..1); }
if !value.contains('/') {
value = format!("**/{}", value);
} else if value.starts_with("./") {
value.drain(..2);
} else if value.starts_with('/') {
value.drain(..1);
}
value
};
let new_pattern = {
let mut value = String::new();
if is_negated {
value.push('!');
}
value.push_str("./");
if !start_pattern.is_empty() {
value.push_str(&start_pattern);
value.push('/');
}
value.push_str(&new_relative_pattern);
value
};
Some(GlobPattern::new(new_pattern, new_base_dir))
} else if let Ok(prefix) = new_base_dir.strip_prefix(&self.base_dir) {
let is_negated = is_negated_glob(&self.relative_pattern);
let mut pattern = non_negated_glob(&self.relative_pattern);
let prefix = prefix.to_string_lossy();
let mut prefix = prefix
.split(if cfg!(windows) { if prefix.contains('\\') { '\\' } else { '/' } } else { '/' })
.collect::<VecDeque<_>>();
loop {
let mut found_sub_match = false;
if pattern.starts_with("**/") {
return Some(GlobPattern::new(
if is_negated { format!("!{}", pattern) } else { pattern.to_string() },
new_base_dir,
));
}
if let Some(new_pattern) = pattern.strip_prefix("*/") {
pattern = new_pattern;
prefix.pop_front();
if prefix.is_empty() {
return Some(GlobPattern::new(
if is_negated { format!("!{}", pattern) } else { pattern.to_string() },
new_base_dir,
));
}
found_sub_match = true;
}
let first_item = prefix.front().unwrap();
if let Some(new_pattern) = pattern.strip_prefix(&format!("{}/", first_item)) {
pattern = new_pattern;
prefix.pop_front();
if prefix.is_empty() {
return Some(GlobPattern::new(
if is_negated { format!("!{}", pattern) } else { pattern.to_string() },
new_base_dir,
));
}
found_sub_match = true;
}
if !found_sub_match {
return None;
}
}
} else {
None
}
}
pub fn as_absolute_pattern_text(&self) -> String {
let is_negated = self.is_negated();
let pattern = non_negated_glob(&self.relative_pattern);
let pattern = pattern.strip_prefix("./").unwrap_or(pattern);
let mut base = self.base_dir.to_string_lossy().to_string();
if cfg!(windows) {
base = base.replace("\\", "/");
}
if !base.ends_with("/") && !pattern.starts_with("/") {
base.push('/');
}
base.push_str(pattern);
if is_negated {
base = format!("!{}", base);
}
base
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn should_invert() {
let test_dir = CanonicalizedPathBuf::new_for_testing("/test");
let pattern = GlobPattern::new("!**/*".to_string(), test_dir.clone()).invert();
assert_eq!(pattern.relative_pattern, "**/*");
let pattern = GlobPattern::new("**/*".to_string(), test_dir).invert();
assert_eq!(pattern.relative_pattern, "!**/*");
}
#[test]
fn should_make_with_new_base() {
let test_dir = CanonicalizedPathBuf::new_for_testing("/test");
let test_dir_dir = CanonicalizedPathBuf::new_for_testing("/test/dir");
let pattern = GlobPattern::new("**/*".to_string(), test_dir_dir.clone());
assert_eq!(pattern.relative_pattern, "**/*");
assert_eq!(pattern.base_dir, test_dir_dir);
let pattern = pattern.into_new_base(test_dir.clone()).unwrap();
assert_eq!(pattern.relative_pattern, "./dir/**/*");
assert_eq!(pattern.base_dir, test_dir);
}
#[test]
fn should_make_with_new_base_when_relative() {
let root_dir = CanonicalizedPathBuf::new_for_testing("/");
let test_dir_dir = CanonicalizedPathBuf::new_for_testing("/test/dir");
let pattern = GlobPattern::new("./**/*".to_string(), test_dir_dir);
let pattern = pattern.into_new_base(root_dir.clone()).unwrap();
assert_eq!(pattern.relative_pattern, "./test/dir/**/*");
assert_eq!(pattern.base_dir, root_dir);
}
#[test]
fn should_make_new_base_when_no_slash() {
let test_dir_dir = CanonicalizedPathBuf::new_for_testing("/test/dir");
let test_dir = CanonicalizedPathBuf::new_for_testing("/test");
let root_dir = CanonicalizedPathBuf::new_for_testing("/");
let pattern = GlobPattern::new("asdf".to_string(), test_dir_dir.clone());
assert_eq!(pattern.relative_pattern, "asdf");
assert_eq!(pattern.base_dir, test_dir_dir);
let pattern = pattern.into_new_base(test_dir.clone()).unwrap();
assert_eq!(pattern.relative_pattern, "./dir/**/asdf");
assert_eq!(pattern.base_dir, test_dir);
let pattern = pattern.into_new_base(root_dir.clone()).unwrap();
assert_eq!(pattern.relative_pattern, "./test/dir/**/asdf");
assert_eq!(pattern.base_dir, root_dir);
}
#[test]
fn should_handle_mapping_into_base_that_is_not_base() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
let pattern = GlobPattern::new("asdf".to_string(), base_dir.clone());
assert_eq!(pattern.relative_pattern, "asdf");
assert_eq!(pattern.base_dir, base_dir);
let sibling_dir = CanonicalizedPathBuf::new_for_testing("/sibling");
assert_eq!(pattern.into_new_base(sibling_dir.clone()), None);
}
#[test]
fn should_handle_mapping_into_parent_dir() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
let pattern = GlobPattern::new("**/*.ts".to_string(), base_dir.clone());
let parent_dir = CanonicalizedPathBuf::new_for_testing("/");
let new_pattern = pattern.into_new_base(parent_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, parent_dir);
assert_eq!(new_pattern.relative_pattern, "./base/**/*.ts");
}
#[test]
fn should_handle_mapping_into_descendant_dir_if_star_star() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
let pattern = GlobPattern::new("**/*.ts".to_string(), base_dir.clone());
{
let child_dir = CanonicalizedPathBuf::new_for_testing("/base/sub");
let new_pattern = pattern.clone().into_new_base(child_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, child_dir);
assert_eq!(new_pattern.relative_pattern, "**/*.ts");
}
{
let grandchild_dir = CanonicalizedPathBuf::new_for_testing("/base/sub/dir");
let new_pattern = pattern.into_new_base(grandchild_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, grandchild_dir);
assert_eq!(new_pattern.relative_pattern, "**/*.ts");
}
{
let pattern = GlobPattern::new("!**/*.ts".to_string(), base_dir.clone());
let grandchild_dir = CanonicalizedPathBuf::new_for_testing("/base/sub/dir");
let new_pattern = pattern.into_new_base(grandchild_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, grandchild_dir);
assert_eq!(new_pattern.relative_pattern, "!**/*.ts");
}
}
#[test]
fn should_handle_mapping_into_child_dir_if_star() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
let pattern = GlobPattern::new("*/*.ts".to_string(), base_dir.clone());
{
let child_dir = CanonicalizedPathBuf::new_for_testing("/base/sub");
let new_pattern = pattern.clone().into_new_base(child_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, child_dir);
assert_eq!(new_pattern.relative_pattern, "*.ts");
}
{
let grandchild_dir = CanonicalizedPathBuf::new_for_testing("/base/sub/dir");
assert_eq!(pattern.into_new_base(grandchild_dir.clone()), None);
}
{
let pattern = GlobPattern::new("!*/*.ts".to_string(), base_dir.clone());
let child_dir = CanonicalizedPathBuf::new_for_testing("/base/sub");
let new_pattern = pattern.into_new_base(child_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, child_dir);
assert_eq!(new_pattern.relative_pattern, "!*.ts");
}
}
#[test]
fn should_handle_mapping_into_dir_if_pattern_matches_name() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
{
let pattern = GlobPattern::new("!sub/*.ts".to_string(), base_dir.clone());
let child_dir = CanonicalizedPathBuf::new_for_testing("/base/sub");
let new_pattern = pattern.clone().into_new_base(child_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, child_dir);
assert_eq!(new_pattern.relative_pattern, "!*.ts");
}
{
let pattern = GlobPattern::new("sub/*/dir/*.ts".to_string(), base_dir.clone());
let descendant_dir = CanonicalizedPathBuf::new_for_testing("/base/sub/something/dir");
let new_pattern = pattern.clone().into_new_base(descendant_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, descendant_dir);
assert_eq!(new_pattern.relative_pattern, "*.ts");
}
{
let pattern = GlobPattern::new("!sub/*/dir/*.ts".to_string(), base_dir.clone());
let descendant_dir = CanonicalizedPathBuf::new_for_testing("/base/sub/something");
let new_pattern = pattern.clone().into_new_base(descendant_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, descendant_dir);
assert_eq!(new_pattern.relative_pattern, "!dir/*.ts");
}
if cfg!(windows) {
let base_dir = CanonicalizedPathBuf::new_for_testing("C:\\base");
let pattern = GlobPattern::new("!sub/*/dir/*.ts".to_string(), base_dir.clone());
let descendant_dir = CanonicalizedPathBuf::new_for_testing("C:\\base\\sub\\something");
let new_pattern = pattern.clone().into_new_base(descendant_dir.clone()).unwrap();
assert_eq!(new_pattern.base_dir, descendant_dir);
assert_eq!(new_pattern.relative_pattern, "!dir/*.ts");
}
}
#[test]
fn into_deepest_base() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
{
let pattern = GlobPattern::new("!sub/*.ts".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base/sub"));
assert_eq!(new_pattern.relative_pattern, "!*.ts");
}
{
let pattern = GlobPattern::new("sub/testing/this/**/out/*.ts".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base/sub/testing/this"));
assert_eq!(new_pattern.relative_pattern, "**/out/*.ts");
}
{
let pattern = GlobPattern::new("testing".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base"));
assert_eq!(new_pattern.relative_pattern, "testing");
}
{
let pattern = GlobPattern::new("sub/testing".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base/sub"));
assert_eq!(new_pattern.relative_pattern, "testing");
}
{
let pattern = GlobPattern::new("testing.js".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base"));
assert_eq!(new_pattern.relative_pattern, "testing.js");
}
{
let pattern = GlobPattern::new("./testing.js".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base"));
assert_eq!(new_pattern.relative_pattern, "./testing.js");
}
{
let pattern = GlobPattern::new("./sub/**/testing.js".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base/sub"));
assert_eq!(new_pattern.relative_pattern, "./**/testing.js");
}
{
let pattern = GlobPattern::new("!./sub/**/testing.js".to_string(), base_dir.clone());
let new_pattern = pattern.into_deepest_base();
assert_eq!(new_pattern.base_dir, CanonicalizedPathBuf::new_for_testing("/base/sub"));
assert_eq!(new_pattern.relative_pattern, "!./**/testing.js");
}
}
#[test]
fn as_absolute_pattern_text() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
{
let pattern = GlobPattern::new("!sub/*.ts".to_string(), base_dir.clone());
assert_eq!(pattern.as_absolute_pattern_text(), "!/base/sub/*.ts");
}
{
let pattern = GlobPattern::new("testing/this/out/*.ts".to_string(), base_dir.clone());
assert_eq!(pattern.as_absolute_pattern_text(), "/base/testing/this/out/*.ts");
}
{
let base_dir = CanonicalizedPathBuf::new_for_testing("/base/");
let pattern = GlobPattern::new("asdf".to_string(), base_dir);
assert_eq!(pattern.as_absolute_pattern_text(), "/base/asdf");
}
{
let pattern = GlobPattern::new("/asdf".to_string(), base_dir.clone());
assert_eq!(pattern.as_absolute_pattern_text(), "/base/asdf");
}
}
#[test]
fn matches_dir_for_traversal() {
let base_dir = CanonicalizedPathBuf::new_for_testing("/base");
{
let pattern = GlobPattern::new("sub/*.ts".to_string(), base_dir.clone());
assert!(pattern.matches_dir_for_traversal(&base_dir.join("sub")));
assert!(!pattern.matches_dir_for_traversal(&base_dir.join("sub/test")));
assert!(!pattern.matches_dir_for_traversal(&base_dir.join("sub/test/no")));
}
{
let pattern = GlobPattern::new("sub/**/testing".to_string(), base_dir.clone());
assert!(pattern.matches_dir_for_traversal(base_dir.as_ref()));
assert!(!pattern.matches_dir_for_traversal(&base_dir.join("other")));
assert!(pattern.matches_dir_for_traversal(&base_dir.join("sub/test")));
assert!(pattern.matches_dir_for_traversal(&base_dir.join("sub/test/yes")));
assert!(pattern.matches_dir_for_traversal(&base_dir.join("sub/test/yes/testing")));
assert!(pattern.matches_dir_for_traversal(&base_dir.join("sub/test/yes/testing/testing/asdf")));
}
{
let pattern = GlobPattern::new("sub/*/testing".to_string(), base_dir.clone());
assert!(pattern.matches_dir_for_traversal(&base_dir.join("sub/test")));
assert!(pattern.matches_dir_for_traversal(&base_dir.join("sub/other")));
assert!(!pattern.matches_dir_for_traversal(&base_dir.join("sub/test/testing")));
assert!(!pattern.matches_dir_for_traversal(&base_dir.join("sub/test/no")));
}
}
}