use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Result;
use crate::arg_parser::FilePatternArgs;
use crate::configuration::ResolvedConfig;
use crate::environment::CanonicalizedPathBuf;
use crate::environment::Environment;
use crate::utils::ExcludeMatchDetail;
use crate::utils::GitIgnoreTree;
use crate::utils::GitIgnoreTreeOptions;
use crate::utils::GlobMatcher;
use crate::utils::GlobMatcherOptions;
use crate::utils::GlobMatchesDetail;
use crate::utils::GlobPattern;
use crate::utils::GlobPatterns;
use crate::utils::expand_braces;
use crate::utils::is_absolute_pattern;
use crate::utils::is_negated_glob;
use crate::utils::non_negated_glob;
use crate::utils::resolve_global_gitignore_lines;
use crate::utils::rewrite_literal_arg_pattern;
use crate::utils::rewrite_literal_arg_patterns;
pub struct FileMatcherOptions<'a> {
pub config: &'a ResolvedConfig,
pub args: &'a FilePatternArgs,
pub root_dir: &'a CanonicalizedPathBuf,
pub specified_file_path: Option<&'a Path>,
pub detect_gitignore_changes: bool,
}
pub struct FileMatcher<TEnvironment: Environment> {
glob_matcher: GlobMatcher,
gitignores: Option<GitIgnoreTree<TEnvironment>>,
}
impl<TEnvironment: Environment> FileMatcher<TEnvironment> {
pub fn new(environment: TEnvironment, opts: FileMatcherOptions) -> Result<Self> {
let FileMatcherOptions {
config,
args,
root_dir,
specified_file_path,
detect_gitignore_changes,
} = opts;
let mut patterns = get_all_file_patterns(config, args, root_dir, &environment);
rewrite_literal_arg_patterns(&environment, &mut patterns, &config.base_path);
let gitignores = if args.no_gitignore {
None
} else {
let global_gitignore_lines = resolve_global_gitignore_lines(&environment);
let mut include_paths = patterns.include_paths();
if let Some(path) = specified_file_path {
include_paths.push(path.to_path_buf());
}
Some(GitIgnoreTree::new(
environment,
GitIgnoreTreeOptions {
include_paths,
global_gitignore_lines,
detect_changes: detect_gitignore_changes,
},
))
};
let glob_matcher = GlobMatcher::new(
patterns,
&GlobMatcherOptions {
case_sensitive: true,
base_dir: config.base_path.clone(),
},
)?;
Ok(FileMatcher { glob_matcher, gitignores })
}
pub fn matches_and_dir_not_ignored(&mut self, file_path: &Path) -> bool {
let match_result = self.glob_matcher.matches_detail(file_path);
if matches!(match_result, GlobMatchesDetail::Excluded | GlobMatchesDetail::NotMatched) {
return false;
}
if let Some(gitignores) = self.gitignores.as_mut() {
gitignores.refresh_for_file(file_path);
}
if matches!(match_result, GlobMatchesDetail::Matched) && self.is_gitignored(file_path, false) {
return false;
}
if !file_path.starts_with(self.glob_matcher.base_dir()) {
return false;
}
for ancestor in file_path.ancestors().skip(1) {
if ancestor == self.glob_matcher.base_dir().as_ref() {
break;
}
if let Ok(path) = ancestor.strip_prefix(self.glob_matcher.base_dir()) {
match self.glob_matcher.check_exclude(path, true) {
ExcludeMatchDetail::Excluded => return false,
ExcludeMatchDetail::OptedOutExclude => {}
ExcludeMatchDetail::NotExcluded => {
if self.is_gitignored(ancestor, true) {
return false;
}
}
}
} else {
break;
}
}
true
}
fn is_gitignored(&mut self, path: &Path, is_dir: bool) -> bool {
let Some(gitignores) = self.gitignores.as_mut() else {
return false;
};
let Some(gitignore) = gitignores.get_resolved_git_ignore_for_file(path) else {
return false;
};
gitignore.is_ignored(path, is_dir)
}
}
pub struct OrderedPatternsMatcher {
matcher: GlobMatcher,
}
impl OrderedPatternsMatcher {
pub fn new(patterns: &[String], config_base_path: &CanonicalizedPathBuf) -> Result<Self> {
let matcher = GlobMatcher::new(
GlobPatterns {
shebangs: Vec::new(),
arg_includes: None,
config_includes: None,
arg_excludes: None,
config_excludes: process_config_patterns(patterns)
.map(|pattern| new_config_glob_pattern(pattern, config_base_path).invert())
.collect(),
},
&GlobMatcherOptions {
case_sensitive: true,
base_dir: config_base_path.clone(),
},
)?;
Ok(Self { matcher })
}
pub fn matches(&self, path: impl AsRef<Path>) -> bool {
self.matcher.exclude_detail(path) == ExcludeMatchDetail::OptedOutExclude
}
pub fn is_excluded(&self, path: impl AsRef<Path>) -> bool {
self.matcher.exclude_detail(path) == ExcludeMatchDetail::Excluded
}
}
pub fn get_all_file_patterns(config: &ResolvedConfig, args: &FilePatternArgs, cwd: &CanonicalizedPathBuf, environment: &impl Environment) -> GlobPatterns {
GlobPatterns {
config_includes: get_config_includes_file_patterns(config, args, cwd, environment),
arg_includes: args
.include_patterns
.as_ref()
.map(|patterns| process_cli_patterns(patterns, cwd, environment).collect()),
config_excludes: get_config_exclude_file_patterns(config, args, cwd, environment),
arg_excludes: if args.exclude_patterns.is_empty() {
None
} else {
Some(process_cli_patterns(&args.exclude_patterns, cwd, environment).collect())
},
shebangs: match (&args.include_pattern_overrides, &config.shebangs) {
(None, Some(shebangs)) => shebangs.keys().cloned().collect(),
_ => Vec::new(),
},
}
}
fn get_config_includes_file_patterns(
config: &ResolvedConfig,
args: &FilePatternArgs,
cwd: &CanonicalizedPathBuf,
environment: &impl Environment,
) -> Option<Vec<GlobPattern>> {
let mut file_patterns = Vec::new();
file_patterns.extend(match &args.include_pattern_overrides {
Some(includes_overrides) => {
process_cli_override_patterns(includes_overrides, cwd, config, environment)
}
None => new_config_glob_patterns(process_config_patterns(config.includes.as_ref()?), &config.base_path),
});
Some(file_patterns)
}
fn get_config_exclude_file_patterns(
config: &ResolvedConfig,
args: &FilePatternArgs,
cwd: &CanonicalizedPathBuf,
environment: &impl Environment,
) -> Vec<GlobPattern> {
let mut file_patterns = Vec::new();
if !args.allow_node_modules && !is_in_node_modules(cwd.as_ref()) {
let node_modules_exclude = String::from("**/node_modules");
file_patterns.push(GlobPattern::new(node_modules_exclude.clone(), cwd.clone()));
if config.base_path != *cwd {
file_patterns.push(GlobPattern::new(node_modules_exclude, config.base_path.clone()));
}
}
file_patterns.extend(match &args.exclude_pattern_overrides {
Some(exclude_overrides) => {
process_cli_override_patterns(exclude_overrides, cwd, config, environment)
}
None => config
.excludes
.as_ref()
.map(|excludes| new_config_glob_patterns(process_config_patterns(excludes), &config.base_path))
.unwrap_or_default(),
});
file_patterns
}
pub fn process_cli_path_args(paths: &[PathBuf], cwd: &CanonicalizedPathBuf, environment: &impl Environment) -> Vec<GlobPattern> {
paths
.iter()
.map(|path| process_cli_pattern(&path.to_string_lossy(), cwd, environment))
.collect()
}
fn is_in_node_modules(dir: &Path) -> bool {
dir.components().any(|component| component.as_os_str() == "node_modules")
}
fn process_file_pattern_slashes(file_pattern: &str) -> String {
file_pattern.replace('\\', "/")
}
fn process_cli_override_patterns(
file_patterns: &[String],
cwd: &CanonicalizedPathBuf,
config: &ResolvedConfig,
environment: &impl Environment,
) -> Vec<GlobPattern> {
process_cli_patterns(file_patterns, cwd, environment)
.map(|mut pattern| {
rewrite_literal_arg_pattern(environment, &mut pattern, &config.base_path);
pattern
})
.collect()
}
fn process_cli_patterns<'a>(
file_patterns: &'a [String],
cwd: &'a CanonicalizedPathBuf,
environment: &'a impl Environment,
) -> impl Iterator<Item = GlobPattern> + 'a {
file_patterns
.iter()
.flat_map(move |pattern| {
let pattern = process_file_pattern_slashes(pattern);
let is_existing_path = pattern.contains('{') && environment.path_exists(cwd.join(non_negated_glob(&pattern)));
let (literal, pattern) = if is_existing_path { (Some(pattern), None) } else { (None, Some(pattern)) };
literal.into_iter().chain(pattern.into_iter().flat_map(expand_braces))
})
.map(move |pattern| process_cli_pattern(&pattern, cwd, environment))
}
fn process_cli_pattern(file_pattern: &str, cwd: &CanonicalizedPathBuf, environment: &impl Environment) -> GlobPattern {
let file_pattern = process_file_pattern_slashes(file_pattern);
let is_negated = is_negated_glob(&file_pattern);
let pattern = non_negated_glob(&file_pattern);
if pattern == "." {
return GlobPattern::new(if is_negated { "!./." } else { "**" }.to_string(), cwd.clone());
}
let absolute_pattern = normalize_path(if is_absolute_pattern(&file_pattern) {
PathBuf::from(pattern)
} else {
cwd.join(pattern)
});
let mut base_dir = cwd.clone();
loop {
if let Ok(relative_pattern) = absolute_pattern.strip_prefix(base_dir.as_ref()) {
return build_cli_pattern(relative_pattern, is_negated, base_dir);
}
let Some(parent) = base_dir.parent() else {
break;
};
base_dir = parent;
}
if let Some(root_dir) = absolute_pattern.ancestors().last().filter(|p| !p.as_os_str().is_empty())
&& let Ok(relative_pattern) = absolute_pattern.strip_prefix(root_dir)
&& let Ok(root_dir) = environment.canonicalize(root_dir)
{
return build_cli_pattern(relative_pattern, is_negated, root_dir);
}
GlobPattern::new(file_pattern, cwd.clone())
}
fn build_cli_pattern(relative_pattern: &Path, is_negated: bool, base_dir: CanonicalizedPathBuf) -> GlobPattern {
let relative_pattern = process_file_pattern_slashes(&relative_pattern.to_string_lossy());
let relative_pattern = format!("{}./{}", if is_negated { "!" } else { "" }, relative_pattern);
GlobPattern::new(relative_pattern, base_dir)
}
fn normalize_path(path: PathBuf) -> PathBuf {
let mut result = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
result.pop();
}
component => result.push(component.as_os_str()),
}
}
result
}
pub fn new_config_glob_patterns(patterns: impl IntoIterator<Item = String>, config_base_path: &CanonicalizedPathBuf) -> Vec<GlobPattern> {
patterns.into_iter().map(|pattern| new_config_glob_pattern(pattern, config_base_path)).collect()
}
pub fn new_config_glob_pattern(pattern: String, config_base_path: &CanonicalizedPathBuf) -> GlobPattern {
let is_negated = is_negated_glob(&pattern);
let non_negated = non_negated_glob(&pattern);
let mut remaining = non_negated.strip_prefix("./").unwrap_or(non_negated);
if !remaining.starts_with("../") {
return GlobPattern::new(pattern, config_base_path.clone());
}
let mut base_dir = config_base_path.clone();
while let Some(rest) = remaining.strip_prefix("../") {
if let Some(parent) = base_dir.parent() {
base_dir = parent;
}
remaining = rest;
}
let relative_pattern = format!("{}./{}", if is_negated { "!" } else { "" }, remaining);
GlobPattern::new(relative_pattern, base_dir)
}
pub fn process_config_patterns(file_patterns: &[String]) -> impl Iterator<Item = String> + '_ {
file_patterns.iter().flat_map(|p| process_config_pattern(p))
}
pub fn process_config_pattern(file_pattern: &str) -> impl Iterator<Item = String> + use<> {
expand_braces(process_file_pattern_slashes(file_pattern)).map(process_expanded_config_pattern)
}
fn process_expanded_config_pattern(mut file_pattern: String) -> String {
if file_pattern.starts_with('/') {
file_pattern.insert(0, '.');
} else if file_pattern.starts_with("!/") {
file_pattern.insert(1, '.');
}
file_pattern
}
#[cfg(test)]
mod test {
use std::path::PathBuf;
use crate::environment::TestEnvironment;
use super::*;
#[test]
fn should_get_if_in_node_modules() {
assert!(is_in_node_modules(Path::new("/node_modules")));
assert!(is_in_node_modules(Path::new("/node_modules/pkg")));
assert!(is_in_node_modules(Path::new("/a/node_modules/pkg/sub")));
assert!(!is_in_node_modules(Path::new("/")));
assert!(!is_in_node_modules(Path::new("/a/sub")));
assert!(!is_in_node_modules(Path::new("/a/my_node_modules/pkg")));
assert!(!is_in_node_modules(Path::new("/a/node_modules_old")));
}
#[test]
fn should_process_cli_patterns() {
assert_cli_pattern("/test", "/", "./test", "/");
assert_cli_pattern("./test", "/", "./test", "/");
assert_cli_pattern("test", "/", "./test", "/");
assert_cli_pattern("**/test", "/", "./**/test", "/");
assert_cli_pattern("!/test", "/", "!./test", "/");
assert_cli_pattern("!./test", "/", "!./test", "/");
assert_cli_pattern("!test", "/", "!./test", "/");
assert_cli_pattern("!**/test", "/", "!./**/test", "/");
assert_cli_pattern("!.", "/", "!./.", "/");
assert_cli_pattern("../test", "/sub", "./test", "/");
assert_cli_pattern("/test", "/sub", "./test", "/");
}
#[cfg(windows)]
#[test]
fn should_process_cli_patterns_windows() {
assert_cli_pattern("C:/test", "C:\\", "./test", "C:\\");
assert_cli_pattern("C:/test/other", "C:\\test\\", "./other", "C:\\test\\");
assert_cli_pattern("C:/test/other", "C:\\test", "./other", "C:\\test");
assert_cli_pattern("../test", "C:\\sub", "./test", "C:\\");
{
let environment = TestEnvironment::new();
let pattern = process_cli_pattern("V:/test/file.txt", &CanonicalizedPathBuf::new_for_testing("C:\\sub"), &environment);
assert_eq!(pattern.relative_pattern, "./test/file.txt");
assert_eq!(pattern.base_dir, environment.canonicalize("V:/").unwrap());
}
assert_cli_pattern("!C:/test", "C:\\", "!./test", "C:\\");
assert_cli_pattern("!C:/test/other", "C:\\test\\", "!./other", "C:\\test\\");
}
#[test]
fn should_expand_brace_groups_in_cli_patterns() {
let environment = TestEnvironment::new();
environment.mk_dir_all("/sub/dir{a").unwrap();
environment.write_file("/sub/dir{a/b,c}.txt", "").unwrap();
let cwd = CanonicalizedPathBuf::new_for_testing("/sub");
let process = |pattern: &str| {
process_cli_patterns(&[pattern.to_string()], &cwd, &environment)
.map(|p| (p.relative_pattern, p.base_dir.to_string_lossy().replace('\\', "/")))
.collect::<Vec<_>>()
};
let sub = |pattern: &str| (pattern.to_string(), "/sub".to_string());
let root = |pattern: &str| (pattern.to_string(), "/".to_string());
assert_eq!(process("{.,src/**}/*.js"), [sub("./*.js"), sub("./src/**/*.js")]);
assert_eq!(process("!{.,src/**}/*.js"), [sub("!./*.js"), sub("!./src/**/*.js")]);
assert_eq!(process("{../a,b}/*.js"), [root("./a/*.js"), sub("./b/*.js")]);
assert_eq!(process("{,src/dir}/*.js"), [sub("./*.js"), sub("./src/dir/*.js")]);
assert_eq!(process("dir{a/b,c}.txt"), [sub("./dir{a/b,c}.txt")]);
assert_eq!(process("!dir{a/b,c}.txt"), [sub("!./dir{a/b,c}.txt")]);
assert_eq!(process("other{a/b,c}.txt"), [sub("./othera/b.txt"), sub("./otherc.txt")]);
}
#[track_caller]
fn assert_cli_pattern(file_pattern: &str, cwd: &str, expected_pattern: &str, expected_base_dir: &str) {
let environment = TestEnvironment::new();
let pattern = process_cli_pattern(file_pattern, &CanonicalizedPathBuf::new_for_testing(cwd), &environment);
assert_eq!(pattern.relative_pattern, expected_pattern);
assert_eq!(pattern.base_dir, CanonicalizedPathBuf::new_for_testing(expected_base_dir));
}
#[test]
fn should_create_config_glob_pattern_relative_to_config_dir() {
let base = CanonicalizedPathBuf::new_for_testing("/a/b");
let pattern = |text: &str| {
let pattern = new_config_glob_pattern(text.to_string(), &base);
(pattern.relative_pattern, pattern.base_dir.to_string_lossy().replace('\\', "/"))
};
assert_eq!(pattern("src/**/*.ts"), ("src/**/*.ts".to_string(), "/a/b".to_string()));
assert_eq!(pattern("./src"), ("./src".to_string(), "/a/b".to_string()));
assert_eq!(pattern("../src/**"), ("./src/**".to_string(), "/a".to_string()));
assert_eq!(pattern("./../src"), ("./src".to_string(), "/a".to_string()));
assert_eq!(pattern("!../src"), ("!./src".to_string(), "/a".to_string()));
assert_eq!(pattern("../../**/Cargo.toml"), ("./**/Cargo.toml".to_string(), "/".to_string()));
assert_eq!(pattern("../../../src"), ("./src".to_string(), "/".to_string()));
}
#[test]
fn should_process_config_pattern() {
let process = |pattern: &str| process_config_pattern(pattern).collect::<Vec<_>>();
assert_eq!(process("/test"), ["./test"]);
assert_eq!(process("./test"), ["./test"]);
assert_eq!(process("test"), ["test"]);
assert_eq!(process("**/test"), ["**/test"]);
assert_eq!(process("!/test"), ["!./test"]);
assert_eq!(process("!./test"), ["!./test"]);
assert_eq!(process("!test"), ["!test"]);
assert_eq!(process("!**/test"), ["!**/test"]);
assert_eq!(process("{/test,sub/**}/*.js"), ["./test/*.js", "sub/**/*.js"]);
assert_eq!(process("!{/test,sub/**}/*.js"), ["!./test/*.js", "!sub/**/*.js"]);
}
#[test]
fn should_match_brace_groups_spanning_path_components() {
let cwd = CanonicalizedPathBuf::new_for_testing("/testing/dir");
let new_matcher = |includes: &[&str], excludes: &[&str]| {
let to_patterns = |patterns: &[&str]| {
let patterns = patterns.iter().map(|p| p.to_string()).collect::<Vec<_>>();
new_config_glob_patterns(process_config_patterns(&patterns), &cwd)
};
GlobMatcher::new(
GlobPatterns {
shebangs: Vec::new(),
arg_includes: None,
config_includes: Some(to_patterns(includes)),
arg_excludes: None,
config_excludes: to_patterns(excludes),
},
&GlobMatcherOptions {
case_sensitive: true,
base_dir: cwd.clone(),
},
)
.unwrap()
};
let matcher = new_matcher(&["{.,src/**,worker}/*.js"], &[]);
assert_eq!(matcher.matches_detail("/testing/dir/src/foo/match.js"), GlobMatchesDetail::Matched);
assert_eq!(matcher.matches_detail("/testing/dir/src/match.js"), GlobMatchesDetail::Matched);
assert_eq!(matcher.matches_detail("/testing/dir/match.js"), GlobMatchesDetail::Matched);
assert_eq!(matcher.matches_detail("/testing/dir/worker/match.js"), GlobMatchesDetail::Matched);
assert_eq!(matcher.matches_detail("/testing/dir/foo/not_match.js"), GlobMatchesDetail::NotMatched);
assert_eq!(matcher.matches_detail("/testing/dir/worker/foo/not_match.js"), GlobMatchesDetail::NotMatched);
let matcher = new_matcher(&["**/*.js"], &["{.,src/**}/*.js"]);
assert_eq!(matcher.matches_detail("/testing/dir/match.js"), GlobMatchesDetail::Excluded);
assert_eq!(matcher.matches_detail("/testing/dir/src/match.js"), GlobMatchesDetail::Excluded);
assert_eq!(matcher.matches_detail("/testing/dir/src/foo/match.js"), GlobMatchesDetail::Excluded);
assert_eq!(matcher.matches_detail("/testing/dir/worker/match.js"), GlobMatchesDetail::Matched);
}
#[test]
fn handles_ignored_dir() {
let environment = TestEnvironment::new();
let cwd = CanonicalizedPathBuf::new_for_testing("/testing/dir");
let glob_matcher = GlobMatcher::new(
GlobPatterns {
shebangs: Vec::new(),
arg_includes: None,
config_includes: Some(vec![GlobPattern::new("**/*.ts".to_string(), cwd.clone())]),
arg_excludes: None,
config_excludes: vec![GlobPattern::new("sub-dir".to_string(), cwd.clone())],
},
&GlobMatcherOptions {
case_sensitive: true,
base_dir: cwd,
},
)
.unwrap();
let mut file_matcher = FileMatcher {
glob_matcher,
gitignores: Some(GitIgnoreTree::new(environment, GitIgnoreTreeOptions::default())),
};
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/match.ts", true);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/other/match.ts", true);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/sub-dir/no-match.ts", false);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/sub-dir/nested/no-match.ts", false);
}
#[test]
fn handles_ignored_dir_while_include_is_sub_dir() {
let environment = TestEnvironment::new();
let base_dir = CanonicalizedPathBuf::new_for_testing("/");
let cwd = CanonicalizedPathBuf::new_for_testing("/sub-dir");
environment.mk_dir_all(&cwd).unwrap();
let glob_matcher = GlobMatcher::new(
GlobPatterns {
shebangs: Vec::new(),
arg_includes: None,
config_includes: Some(vec![GlobPattern::new("**/*.ts".to_string(), cwd.clone())]),
arg_excludes: None,
config_excludes: vec![GlobPattern::new("**/dist".to_string(), base_dir.clone())],
},
&GlobMatcherOptions {
case_sensitive: true,
base_dir: cwd,
},
)
.unwrap();
let mut file_matcher = FileMatcher {
glob_matcher,
gitignores: Some(GitIgnoreTree::new(environment, GitIgnoreTreeOptions::default())),
};
assert_matches_dir_and_not_ignored(&mut file_matcher, "/sub-dir/dir/match.ts", true);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/sub-dir/dir/other/match.ts", true);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/sub-dir/dist/no-match.ts", false);
}
#[test]
fn handles_gitignored_ancestor_dir() {
let environment = TestEnvironment::new();
let base_dir = CanonicalizedPathBuf::new_for_testing("/testing/dir");
environment.mk_dir_all(base_dir.as_ref()).unwrap();
environment.write_file("/testing/dir/.gitignore", "ignored-dir/\nsub.ts/\n").unwrap();
let glob_matcher = GlobMatcher::new(
GlobPatterns {
shebangs: Vec::new(),
arg_includes: None,
config_includes: Some(vec![GlobPattern::new("**/*.ts".to_string(), base_dir.clone())]),
arg_excludes: None,
config_excludes: vec![],
},
&GlobMatcherOptions {
case_sensitive: true,
base_dir: base_dir.clone(),
},
)
.unwrap();
let mut file_matcher = FileMatcher {
glob_matcher,
gitignores: Some(GitIgnoreTree::new(environment, GitIgnoreTreeOptions::default())),
};
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/ignored-dir/no-match.ts", false);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/ignored-dir/nested/no-match.ts", false);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/other/match.ts", true);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/sub.ts", true);
}
#[test]
fn ignores_gitignored_base_dir_itself() {
let environment = TestEnvironment::new();
let base_dir = CanonicalizedPathBuf::new_for_testing("/testing/dir");
environment.mk_dir_all(base_dir.as_ref()).unwrap();
environment.write_file("/testing/.gitignore", "dir/\n").unwrap();
let glob_matcher = GlobMatcher::new(
GlobPatterns {
shebangs: Vec::new(),
arg_includes: None,
config_includes: Some(vec![GlobPattern::new("**/*.ts".to_string(), base_dir.clone())]),
arg_excludes: None,
config_excludes: vec![],
},
&GlobMatcherOptions {
case_sensitive: true,
base_dir: base_dir.clone(),
},
)
.unwrap();
let mut file_matcher = FileMatcher {
glob_matcher,
gitignores: Some(GitIgnoreTree::new(environment, GitIgnoreTreeOptions::default())),
};
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/match.ts", true);
assert_matches_dir_and_not_ignored(&mut file_matcher, "/testing/dir/sub/match.ts", true);
}
#[track_caller]
fn assert_matches_dir_and_not_ignored(matcher: &mut FileMatcher<TestEnvironment>, path: &str, expected: bool) {
assert_eq!(matcher.matches_and_dir_not_ignored(&PathBuf::from(path)), expected);
}
}