mirror of
https://github.com/denoland/deno.git
synced 2025-09-27 04:39:10 +00:00

Some checks are pending
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / build libs (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions
Related PR: https://github.com/denoland/deno/pull/30354
1635 lines
47 KiB
Rust
1635 lines
47 KiB
Rust
// Copyright 2018-2025 the Deno authors. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use deno_error::JsError;
|
|
use deno_path_util::normalize_path;
|
|
use deno_path_util::url_to_file_path;
|
|
use indexmap::IndexMap;
|
|
use thiserror::Error;
|
|
use url::Url;
|
|
|
|
use crate::UrlToFilePathError;
|
|
|
|
mod collector;
|
|
mod gitignore;
|
|
|
|
pub use collector::FileCollector;
|
|
pub use collector::WalkEntry;
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum FilePatternsMatch {
|
|
/// File passes as matching, but further exclude matching (ex. .gitignore)
|
|
/// may be necessary.
|
|
Passed,
|
|
/// File passes matching and further exclude matching (ex. .gitignore)
|
|
/// should NOT be done.
|
|
PassedOptedOutExclude,
|
|
/// File was excluded.
|
|
Excluded,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum PathKind {
|
|
File,
|
|
Directory,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub struct FilePatterns {
|
|
/// Default traversal base used when calling `split_by_base()` without
|
|
/// any `include` patterns.
|
|
pub base: PathBuf,
|
|
pub include: Option<PathOrPatternSet>,
|
|
pub exclude: PathOrPatternSet,
|
|
}
|
|
|
|
impl FilePatterns {
|
|
pub fn new_with_base(base: PathBuf) -> Self {
|
|
Self {
|
|
base,
|
|
include: Default::default(),
|
|
exclude: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn with_new_base(self, new_base: PathBuf) -> Self {
|
|
Self {
|
|
base: new_base,
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub fn matches_specifier(&self, specifier: &Url) -> bool {
|
|
self.matches_specifier_detail(specifier) != FilePatternsMatch::Excluded
|
|
}
|
|
|
|
pub fn matches_specifier_detail(&self, specifier: &Url) -> FilePatternsMatch {
|
|
if specifier.scheme() != "file" {
|
|
// can't do .gitignore on a non-file specifier
|
|
return FilePatternsMatch::PassedOptedOutExclude;
|
|
}
|
|
let path = match url_to_file_path(specifier) {
|
|
Ok(path) => path,
|
|
Err(_) => return FilePatternsMatch::PassedOptedOutExclude,
|
|
};
|
|
self.matches_path_detail(&path, PathKind::File) // use file matching behavior
|
|
}
|
|
|
|
pub fn matches_path(&self, path: &Path, path_kind: PathKind) -> bool {
|
|
self.matches_path_detail(path, path_kind) != FilePatternsMatch::Excluded
|
|
}
|
|
|
|
pub fn matches_path_detail(
|
|
&self,
|
|
path: &Path,
|
|
path_kind: PathKind,
|
|
) -> FilePatternsMatch {
|
|
// if there's an include list, only include files that match it
|
|
// the include list is a closed set
|
|
if let Some(include) = &self.include {
|
|
match path_kind {
|
|
PathKind::File => {
|
|
if include.matches_path_detail(path) != PathOrPatternsMatch::Matched {
|
|
return FilePatternsMatch::Excluded;
|
|
}
|
|
}
|
|
PathKind::Directory => {
|
|
// for now ignore the include list unless there's a negated
|
|
// glob for the directory
|
|
for p in include.0.iter().rev() {
|
|
match p.matches_path(path) {
|
|
PathGlobMatch::Matched => {
|
|
break;
|
|
}
|
|
PathGlobMatch::MatchedNegated => {
|
|
return FilePatternsMatch::Excluded;
|
|
}
|
|
PathGlobMatch::NotMatched => {
|
|
// keep going
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// the exclude list is an open set and we skip files not in the exclude list
|
|
match self.exclude.matches_path_detail(path) {
|
|
PathOrPatternsMatch::Matched => FilePatternsMatch::Excluded,
|
|
PathOrPatternsMatch::NotMatched => FilePatternsMatch::Passed,
|
|
PathOrPatternsMatch::Excluded => FilePatternsMatch::PassedOptedOutExclude,
|
|
}
|
|
}
|
|
|
|
/// Creates a collection of `FilePatterns` 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<Self> {
|
|
let negated_excludes = self
|
|
.exclude
|
|
.0
|
|
.iter()
|
|
.filter(|e| e.is_negated())
|
|
.collect::<Vec<_>>();
|
|
let include = match &self.include {
|
|
Some(include) => Cow::Borrowed(include),
|
|
None => {
|
|
if negated_excludes.is_empty() {
|
|
return vec![self.clone()];
|
|
} else {
|
|
Cow::Owned(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
self.base.clone(),
|
|
)]))
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut include_paths = Vec::with_capacity(include.0.len());
|
|
let mut include_patterns = Vec::with_capacity(include.0.len());
|
|
let mut exclude_patterns =
|
|
Vec::with_capacity(include.0.len() + self.exclude.0.len());
|
|
|
|
for path_or_pattern in &include.0 {
|
|
match path_or_pattern {
|
|
PathOrPattern::Path(path) => include_paths.push(path),
|
|
PathOrPattern::NegatedPath(path) => {
|
|
exclude_patterns.push(PathOrPattern::Path(path.clone()));
|
|
}
|
|
PathOrPattern::Pattern(pattern) => {
|
|
if pattern.is_negated() {
|
|
exclude_patterns.push(PathOrPattern::Pattern(pattern.as_negated()));
|
|
} else {
|
|
include_patterns.push(pattern.clone());
|
|
}
|
|
}
|
|
PathOrPattern::RemoteUrl(_) => {}
|
|
}
|
|
}
|
|
|
|
let capacity = include_patterns.len() + negated_excludes.len();
|
|
let mut include_patterns_by_base_path = include_patterns.into_iter().fold(
|
|
IndexMap::with_capacity(capacity),
|
|
|mut map: IndexMap<_, Vec<_>>, p| {
|
|
map.entry(p.base_path()).or_default().push(p);
|
|
map
|
|
},
|
|
);
|
|
for p in &negated_excludes {
|
|
if let Some(base_path) = p.base_path()
|
|
&& !include_patterns_by_base_path.contains_key(&base_path)
|
|
{
|
|
let has_any_base_parent = include_patterns_by_base_path
|
|
.keys()
|
|
.any(|k| base_path.starts_with(k))
|
|
|| include_paths.iter().any(|p| base_path.starts_with(p));
|
|
// don't include an orphaned negated pattern
|
|
if has_any_base_parent {
|
|
include_patterns_by_base_path.insert(base_path, Vec::new());
|
|
}
|
|
}
|
|
}
|
|
|
|
let exclude_by_base_path = exclude_patterns
|
|
.iter()
|
|
.chain(self.exclude.0.iter())
|
|
.filter_map(|s| Some((s.base_path()?, s)))
|
|
.collect::<Vec<_>>();
|
|
let get_applicable_excludes = |base_path: &PathBuf| -> Vec<PathOrPattern> {
|
|
exclude_by_base_path
|
|
.iter()
|
|
.filter_map(|(exclude_base_path, exclude)| {
|
|
match exclude {
|
|
PathOrPattern::RemoteUrl(_) => None,
|
|
PathOrPattern::Path(exclude_path)
|
|
| PathOrPattern::NegatedPath(exclude_path) => {
|
|
// include paths that's are sub paths or an ancestor path
|
|
if 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 an ancestor path
|
|
if exclude_base_path.starts_with(base_path)
|
|
|| base_path.starts_with(exclude_base_path)
|
|
{
|
|
Some((*exclude).clone())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
|
|
let mut result = Vec::with_capacity(
|
|
include_paths.len() + include_patterns_by_base_path.len(),
|
|
);
|
|
for path in include_paths {
|
|
let applicable_excludes = get_applicable_excludes(path);
|
|
result.push(Self {
|
|
base: path.clone(),
|
|
include: if self.include.is_none() {
|
|
None
|
|
} else {
|
|
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(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(Self {
|
|
base: base_path.clone(),
|
|
include: if self.include.is_none()
|
|
|| applicable_includes.is_empty()
|
|
&& self
|
|
.include
|
|
.as_ref()
|
|
.map(|i| !i.0.is_empty())
|
|
.unwrap_or(false)
|
|
{
|
|
None
|
|
} else {
|
|
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| {
|
|
// try looking at the parents first so that files in the same
|
|
// folder are kept in the same order that they're provided
|
|
let (a, b) =
|
|
if let (Some(a), Some(b)) = (a.base.parent(), b.base.parent()) {
|
|
(a, b)
|
|
} else {
|
|
(a.base.as_path(), b.base.as_path())
|
|
};
|
|
b.as_os_str().len().cmp(&a.as_os_str().len())
|
|
});
|
|
|
|
result
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
|
pub enum PathOrPatternsMatch {
|
|
Matched,
|
|
NotMatched,
|
|
Excluded,
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
pub enum FromExcludeRelativePathOrPatternsError {
|
|
#[class(type)]
|
|
#[error(
|
|
"The negation of '{negated_entry}' is never reached due to the higher priority '{entry}' exclude. Move '{negated_entry}' after '{entry}'."
|
|
)]
|
|
HigherPriorityExclude {
|
|
negated_entry: String,
|
|
entry: String,
|
|
},
|
|
#[class(inherit)]
|
|
#[error("{0}")]
|
|
PathOrPatternParse(#[from] PathOrPatternParseError),
|
|
}
|
|
|
|
#[derive(Clone, Default, Debug, Hash, Eq, PartialEq)]
|
|
pub struct PathOrPatternSet(Vec<PathOrPattern>);
|
|
|
|
impl PathOrPatternSet {
|
|
pub fn new(elements: Vec<PathOrPattern>) -> Self {
|
|
Self(elements)
|
|
}
|
|
|
|
pub fn from_absolute_paths(
|
|
paths: &[String],
|
|
) -> Result<Self, PathOrPatternParseError> {
|
|
Ok(Self(
|
|
paths
|
|
.iter()
|
|
.map(|p| PathOrPattern::new(p))
|
|
.collect::<Result<Vec<_>, _>>()?,
|
|
))
|
|
}
|
|
|
|
/// Builds the set of path and patterns for an "include" list.
|
|
pub fn from_include_relative_path_or_patterns(
|
|
base: &Path,
|
|
entries: &[String],
|
|
) -> Result<Self, PathOrPatternParseError> {
|
|
Ok(Self(
|
|
entries
|
|
.iter()
|
|
.map(|p| PathOrPattern::from_relative(base, p))
|
|
.collect::<Result<Vec<_>, _>>()?,
|
|
))
|
|
}
|
|
|
|
/// Builds the set and ensures no negations are overruled by
|
|
/// higher priority entries.
|
|
pub fn from_exclude_relative_path_or_patterns(
|
|
base: &Path,
|
|
entries: &[String],
|
|
) -> Result<Self, FromExcludeRelativePathOrPatternsError> {
|
|
// error when someone does something like:
|
|
// exclude: ["!./a/b", "./a"] as it should be the opposite
|
|
fn validate_entry(
|
|
found_negated_paths: &Vec<(&str, PathBuf)>,
|
|
entry: &str,
|
|
entry_path: &Path,
|
|
) -> Result<(), FromExcludeRelativePathOrPatternsError> {
|
|
for (negated_entry, negated_path) in found_negated_paths {
|
|
if negated_path.starts_with(entry_path) {
|
|
return Err(
|
|
FromExcludeRelativePathOrPatternsError::HigherPriorityExclude {
|
|
negated_entry: negated_entry.to_string(),
|
|
entry: entry.to_string(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
let mut found_negated_paths: Vec<(&str, PathBuf)> =
|
|
Vec::with_capacity(entries.len());
|
|
let mut result = Vec::with_capacity(entries.len());
|
|
for entry in entries {
|
|
let p = PathOrPattern::from_relative(base, entry)?;
|
|
match &p {
|
|
PathOrPattern::Path(p) => {
|
|
validate_entry(&found_negated_paths, entry, p)?;
|
|
}
|
|
PathOrPattern::NegatedPath(p) => {
|
|
found_negated_paths.push((entry.as_str(), p.clone()));
|
|
}
|
|
PathOrPattern::RemoteUrl(_) => {
|
|
// ignore
|
|
}
|
|
PathOrPattern::Pattern(p) => {
|
|
if p.is_negated() {
|
|
let base_path = p.base_path();
|
|
found_negated_paths.push((entry.as_str(), base_path));
|
|
}
|
|
}
|
|
}
|
|
result.push(p);
|
|
}
|
|
Ok(Self(result))
|
|
}
|
|
|
|
pub fn inner(&self) -> &Vec<PathOrPattern> {
|
|
&self.0
|
|
}
|
|
|
|
pub fn inner_mut(&mut self) -> &mut Vec<PathOrPattern> {
|
|
&mut self.0
|
|
}
|
|
|
|
pub fn into_path_or_patterns(self) -> Vec<PathOrPattern> {
|
|
self.0
|
|
}
|
|
|
|
pub fn matches_path(&self, path: &Path) -> bool {
|
|
self.matches_path_detail(path) == PathOrPatternsMatch::Matched
|
|
}
|
|
|
|
pub fn matches_path_detail(&self, path: &Path) -> PathOrPatternsMatch {
|
|
for p in self.0.iter().rev() {
|
|
match p.matches_path(path) {
|
|
PathGlobMatch::Matched => return PathOrPatternsMatch::Matched,
|
|
PathGlobMatch::MatchedNegated => return PathOrPatternsMatch::Excluded,
|
|
PathGlobMatch::NotMatched => {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
PathOrPatternsMatch::NotMatched
|
|
}
|
|
|
|
pub fn base_paths(&self) -> Vec<PathBuf> {
|
|
let mut result = Vec::with_capacity(self.0.len());
|
|
for element in &self.0 {
|
|
match element {
|
|
PathOrPattern::Path(path) | PathOrPattern::NegatedPath(path) => {
|
|
result.push(path.to_path_buf());
|
|
}
|
|
PathOrPattern::RemoteUrl(_) => {
|
|
// ignore
|
|
}
|
|
PathOrPattern::Pattern(pattern) => {
|
|
result.push(pattern.base_path());
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
pub fn push(&mut self, item: PathOrPattern) {
|
|
self.0.push(item);
|
|
}
|
|
|
|
pub fn append(&mut self, items: impl Iterator<Item = PathOrPattern>) {
|
|
self.0.extend(items)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError, Clone)]
|
|
#[class(inherit)]
|
|
#[error("Invalid URL '{}'", url)]
|
|
pub struct UrlParseError {
|
|
url: String,
|
|
#[source]
|
|
#[inherit]
|
|
source: url::ParseError,
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
pub enum PathOrPatternParseError {
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
UrlParse(#[from] UrlParseError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
UrlToFilePathError(#[from] UrlToFilePathError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
GlobParse(#[from] GlobPatternParseError),
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
|
pub enum PathOrPattern {
|
|
Path(PathBuf),
|
|
NegatedPath(PathBuf),
|
|
RemoteUrl(Url),
|
|
Pattern(GlobPattern),
|
|
}
|
|
|
|
impl PathOrPattern {
|
|
pub fn new(path: &str) -> Result<Self, PathOrPatternParseError> {
|
|
if has_url_prefix(path) {
|
|
let url = Url::parse(path).map_err(|err| UrlParseError {
|
|
url: path.to_string(),
|
|
source: err,
|
|
})?;
|
|
if url.scheme() == "file" {
|
|
let path = url_to_file_path(&url)?;
|
|
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(Cow::Borrowed(Path::new(path))).into_owned(),
|
|
)
|
|
})
|
|
})
|
|
.map_err(|err| err.into())
|
|
}
|
|
|
|
pub fn from_relative(
|
|
base: &Path,
|
|
p: &str,
|
|
) -> Result<PathOrPattern, PathOrPatternParseError> {
|
|
if is_glob_pattern(p) {
|
|
GlobPattern::from_relative(base, p)
|
|
.map(PathOrPattern::Pattern)
|
|
.map_err(|err| err.into())
|
|
} else if has_url_prefix(p) {
|
|
PathOrPattern::new(p)
|
|
} else if let Some(path) = p.strip_prefix('!') {
|
|
Ok(PathOrPattern::NegatedPath(
|
|
normalize_path(Cow::Owned(base.join(path))).into_owned(),
|
|
))
|
|
} else {
|
|
Ok(PathOrPattern::Path(
|
|
normalize_path(Cow::Owned(base.join(p))).into_owned(),
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn matches_path(&self, path: &Path) -> PathGlobMatch {
|
|
match self {
|
|
PathOrPattern::Path(p) => {
|
|
if path.starts_with(p) {
|
|
PathGlobMatch::Matched
|
|
} else {
|
|
PathGlobMatch::NotMatched
|
|
}
|
|
}
|
|
PathOrPattern::NegatedPath(p) => {
|
|
if path.starts_with(p) {
|
|
PathGlobMatch::MatchedNegated
|
|
} else {
|
|
PathGlobMatch::NotMatched
|
|
}
|
|
}
|
|
PathOrPattern::RemoteUrl(_) => PathGlobMatch::NotMatched,
|
|
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<PathBuf> {
|
|
match self {
|
|
PathOrPattern::Path(p) | PathOrPattern::NegatedPath(p) => Some(p.clone()),
|
|
PathOrPattern::RemoteUrl(_) => None,
|
|
PathOrPattern::Pattern(p) => Some(p.base_path()),
|
|
}
|
|
}
|
|
|
|
/// If this is a negated pattern.
|
|
pub fn is_negated(&self) -> bool {
|
|
match self {
|
|
PathOrPattern::Path(_) => false,
|
|
PathOrPattern::NegatedPath(_) => true,
|
|
PathOrPattern::RemoteUrl(_) => false,
|
|
PathOrPattern::Pattern(p) => p.is_negated(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum PathGlobMatch {
|
|
Matched,
|
|
MatchedNegated,
|
|
NotMatched,
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
#[class(type)]
|
|
#[error("Failed to expand glob: \"{pattern}\"")]
|
|
pub struct GlobPatternParseError {
|
|
pattern: String,
|
|
#[source]
|
|
source: glob::PatternError,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct GlobPattern {
|
|
is_negated: bool,
|
|
pattern: glob::Pattern,
|
|
}
|
|
|
|
impl GlobPattern {
|
|
pub fn new_if_pattern(
|
|
pattern: &str,
|
|
) -> Result<Option<Self>, GlobPatternParseError> {
|
|
if !is_glob_pattern(pattern) {
|
|
return Ok(None);
|
|
}
|
|
Self::new(pattern).map(Some)
|
|
}
|
|
|
|
pub fn new(pattern: &str) -> Result<Self, GlobPatternParseError> {
|
|
let (is_negated, pattern) = match pattern.strip_prefix('!') {
|
|
Some(pattern) => (true, pattern),
|
|
None => (false, pattern),
|
|
};
|
|
let pattern = escape_brackets(pattern).replace('\\', "/");
|
|
let pattern =
|
|
glob::Pattern::new(&pattern).map_err(|source| GlobPatternParseError {
|
|
pattern: pattern.to_string(),
|
|
source,
|
|
})?;
|
|
Ok(Self {
|
|
is_negated,
|
|
pattern,
|
|
})
|
|
}
|
|
|
|
pub fn from_relative(
|
|
base: &Path,
|
|
p: &str,
|
|
) -> Result<Self, GlobPatternParseError> {
|
|
let (is_negated, p) = match p.strip_prefix('!') {
|
|
Some(p) => (true, p),
|
|
None => (false, p),
|
|
};
|
|
let base_str = base.to_string_lossy().replace('\\', "/");
|
|
let p = p.strip_prefix("./").unwrap_or(p);
|
|
let p = p.strip_suffix('/').unwrap_or(p);
|
|
let pattern = capacity_builder::StringBuilder::<String>::build(|builder| {
|
|
if is_negated {
|
|
builder.append('!');
|
|
}
|
|
builder.append(&base_str);
|
|
if !base_str.ends_with('/') {
|
|
builder.append('/');
|
|
}
|
|
builder.append(p);
|
|
})
|
|
.unwrap();
|
|
GlobPattern::new(&pattern)
|
|
}
|
|
|
|
pub fn as_str(&self) -> Cow<'_, str> {
|
|
if self.is_negated {
|
|
Cow::Owned(format!("!{}", self.pattern.as_str()))
|
|
} else {
|
|
Cow::Borrowed(self.pattern.as_str())
|
|
}
|
|
}
|
|
|
|
pub fn matches_path(&self, path: &Path) -> PathGlobMatch {
|
|
if self.pattern.matches_path_with(path, match_options()) {
|
|
if self.is_negated {
|
|
PathGlobMatch::MatchedNegated
|
|
} else {
|
|
PathGlobMatch::Matched
|
|
}
|
|
} else {
|
|
PathGlobMatch::NotMatched
|
|
}
|
|
}
|
|
|
|
pub fn base_path(&self) -> PathBuf {
|
|
let base_path = self
|
|
.pattern
|
|
.as_str()
|
|
.split('/')
|
|
.take_while(|c| !has_glob_chars(c))
|
|
.collect::<Vec<_>>()
|
|
.join(std::path::MAIN_SEPARATOR_STR);
|
|
PathBuf::from(base_path)
|
|
}
|
|
|
|
pub fn is_negated(&self) -> bool {
|
|
self.is_negated
|
|
}
|
|
|
|
fn as_negated(&self) -> GlobPattern {
|
|
Self {
|
|
is_negated: !self.is_negated,
|
|
pattern: self.pattern.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn is_glob_pattern(path: &str) -> bool {
|
|
!has_url_prefix(path) && has_glob_chars(path)
|
|
}
|
|
|
|
fn has_url_prefix(pattern: &str) -> bool {
|
|
pattern.starts_with("http://")
|
|
|| pattern.starts_with("https://")
|
|
|| pattern.starts_with("file://")
|
|
|| pattern.starts_with("npm:")
|
|
|| pattern.starts_with("jsr:")
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::error::Error;
|
|
|
|
use deno_path_util::url_from_directory_path;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
|
|
use super::*;
|
|
|
|
// For easier comparisons in tests.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
struct ComparableFilePatterns {
|
|
base: String,
|
|
include: Option<Vec<String>>,
|
|
exclude: Vec<String>,
|
|
}
|
|
|
|
impl ComparableFilePatterns {
|
|
pub fn new(root: &Path, file_patterns: &FilePatterns) -> Self {
|
|
fn path_to_string(root: &Path, path: &Path) -> String {
|
|
path
|
|
.strip_prefix(root)
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.replace('\\', "/")
|
|
}
|
|
|
|
fn path_or_pattern_to_string(
|
|
root: &Path,
|
|
p: &PathOrPattern,
|
|
) -> Option<String> {
|
|
match p {
|
|
PathOrPattern::RemoteUrl(_) => None,
|
|
PathOrPattern::Path(p) => Some(path_to_string(root, p)),
|
|
PathOrPattern::NegatedPath(p) => {
|
|
Some(format!("!{}", path_to_string(root, p)))
|
|
}
|
|
PathOrPattern::Pattern(p) => {
|
|
let was_negated = p.is_negated();
|
|
let p = if was_negated {
|
|
p.as_negated()
|
|
} else {
|
|
p.clone()
|
|
};
|
|
let text = p
|
|
.as_str()
|
|
.strip_prefix(&format!(
|
|
"{}/",
|
|
root.to_string_lossy().replace('\\', "/")
|
|
))
|
|
.unwrap_or_else(|| panic!("pattern: {:?}, root: {:?}", p, root))
|
|
.to_string();
|
|
Some(if was_negated {
|
|
format!("!{}", text)
|
|
} else {
|
|
text
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
Self {
|
|
base: path_to_string(root, &file_patterns.base),
|
|
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: &[FilePatterns],
|
|
) -> Vec<ComparableFilePatterns> {
|
|
patterns_by_base
|
|
.iter()
|
|
.map(|file_patterns| ComparableFilePatterns::new(root, file_patterns))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_split_by_base_dir() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let patterns = FilePatterns {
|
|
base: temp_dir.path().to_path_buf(),
|
|
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::from_relative(temp_dir.path(), "!./other/**/*.ts")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "sub/file.ts").unwrap(),
|
|
])),
|
|
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![
|
|
ComparableFilePatterns {
|
|
base: "inner/sub/deeper".to_string(),
|
|
include: Some(vec![
|
|
"inner/sub/deeper/**/*.js".to_string(),
|
|
"inner/**/*.ts".to_string(),
|
|
]),
|
|
exclude: vec!["inner/sub/deeper/file.js".to_string()],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "sub/file.ts".to_string(),
|
|
include: Some(vec!["sub/file.ts".to_string()]),
|
|
exclude: vec![],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "inner".to_string(),
|
|
include: Some(vec!["inner/**/*.ts".to_string()]),
|
|
exclude: vec![
|
|
"inner/other/**/*.ts".to_string(),
|
|
"inner/sub/deeper/file.js".to_string(),
|
|
],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "other".to_string(),
|
|
include: Some(vec!["other/**/*.js".to_string()]),
|
|
exclude: vec!["other/**/*.ts".to_string()],
|
|
}
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_split_by_base_dir_unexcluded() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let patterns = FilePatterns {
|
|
base: temp_dir.path().to_path_buf(),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::from_relative(temp_dir.path(), "./ignored").unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./ignored/unexcluded")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./ignored/test/**")
|
|
.unwrap(),
|
|
]),
|
|
};
|
|
let split = ComparableFilePatterns::from_split(
|
|
temp_dir.path(),
|
|
&patterns.split_by_base(),
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
vec![
|
|
ComparableFilePatterns {
|
|
base: "ignored/unexcluded".to_string(),
|
|
include: None,
|
|
exclude: vec![
|
|
// still keeps the higher level exclude for cases
|
|
// where these two are accidentally swapped
|
|
"ignored".to_string(),
|
|
// keep the glob for the current dir because it
|
|
// could be used to override the .gitignore
|
|
"!ignored/unexcluded".to_string(),
|
|
],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "ignored/test".to_string(),
|
|
include: None,
|
|
exclude: vec!["ignored".to_string(), "!ignored/test/**".to_string(),],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "".to_string(),
|
|
include: None,
|
|
exclude: vec![
|
|
"ignored".to_string(),
|
|
"!ignored/unexcluded".to_string(),
|
|
"!ignored/test/**".to_string(),
|
|
],
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_split_by_base_dir_unexcluded_with_path_includes() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let patterns = FilePatterns {
|
|
base: temp_dir.path().to_path_buf(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::from_relative(temp_dir.path(), "./sub").unwrap(),
|
|
])),
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/**")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "./orphan").unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./orphan/test/**")
|
|
.unwrap(),
|
|
]),
|
|
};
|
|
let split = ComparableFilePatterns::from_split(
|
|
temp_dir.path(),
|
|
&patterns.split_by_base(),
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
vec![
|
|
ComparableFilePatterns {
|
|
base: "sub/ignored/test".to_string(),
|
|
include: None,
|
|
exclude: vec![
|
|
"sub/ignored".to_string(),
|
|
"!sub/ignored/test/**".to_string(),
|
|
],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "sub".to_string(),
|
|
include: Some(vec!["sub".to_string()]),
|
|
exclude: vec![
|
|
"sub/ignored".to_string(),
|
|
"!sub/ignored/test/**".to_string(),
|
|
],
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_split_by_base_dir_unexcluded_with_glob_includes() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let patterns = FilePatterns {
|
|
base: temp_dir.path().to_path_buf(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::from_relative(temp_dir.path(), "./sub/**").unwrap(),
|
|
])),
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/**")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./orphan/test/**")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!orphan/other").unwrap(),
|
|
]),
|
|
};
|
|
let split = ComparableFilePatterns::from_split(
|
|
temp_dir.path(),
|
|
&patterns.split_by_base(),
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
vec![
|
|
ComparableFilePatterns {
|
|
base: "sub/ignored/test".to_string(),
|
|
include: Some(vec!["sub/**".to_string()]),
|
|
exclude: vec![
|
|
"sub/ignored".to_string(),
|
|
"!sub/ignored/test/**".to_string()
|
|
],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "sub".to_string(),
|
|
include: Some(vec!["sub/**".to_string()]),
|
|
exclude: vec![
|
|
"sub/ignored".to_string(),
|
|
"!sub/ignored/test/**".to_string(),
|
|
],
|
|
}
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_split_by_base_dir_opposite_exclude() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let patterns = FilePatterns {
|
|
base: temp_dir.path().to_path_buf(),
|
|
include: None,
|
|
// this will actually error before it gets here in integration,
|
|
// but it's best to ensure it's handled anyway
|
|
exclude: PathOrPatternSet::new(vec![
|
|
// this won't be unexcluded because it's lower priority than the entry below
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/")
|
|
.unwrap(),
|
|
// this is higher priority
|
|
PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
|
|
]),
|
|
};
|
|
let split = ComparableFilePatterns::from_split(
|
|
temp_dir.path(),
|
|
&patterns.split_by_base(),
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
vec![
|
|
ComparableFilePatterns {
|
|
base: "sub/ignored/test".to_string(),
|
|
include: None,
|
|
exclude: vec![
|
|
"!sub/ignored/test".to_string(),
|
|
"sub/ignored".to_string(),
|
|
],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "".to_string(),
|
|
include: None,
|
|
exclude: vec![
|
|
"!sub/ignored/test".to_string(),
|
|
"sub/ignored".to_string(),
|
|
],
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_split_by_base_dir_exclude_unexcluded_and_glob() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let patterns = FilePatterns {
|
|
base: temp_dir.path().to_path_buf(),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored/**/*.ts")
|
|
.unwrap(),
|
|
]),
|
|
};
|
|
let split = ComparableFilePatterns::from_split(
|
|
temp_dir.path(),
|
|
&patterns.split_by_base(),
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
vec![
|
|
ComparableFilePatterns {
|
|
base: "sub/ignored/test".to_string(),
|
|
include: None,
|
|
exclude: vec![
|
|
"sub/ignored".to_string(),
|
|
"!sub/ignored/test".to_string(),
|
|
"sub/ignored/**/*.ts".to_string()
|
|
],
|
|
},
|
|
ComparableFilePatterns {
|
|
base: "".to_string(),
|
|
include: None,
|
|
exclude: vec![
|
|
"sub/ignored".to_string(),
|
|
"!sub/ignored/test".to_string(),
|
|
"sub/ignored/**/*.ts".to_string(),
|
|
],
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn run_file_patterns_match_test(
|
|
file_patterns: &FilePatterns,
|
|
path: &Path,
|
|
kind: PathKind,
|
|
expected: FilePatternsMatch,
|
|
) {
|
|
assert_eq!(
|
|
file_patterns.matches_path_detail(path, kind),
|
|
expected,
|
|
"path: {:?}, kind: {:?}",
|
|
path,
|
|
kind
|
|
);
|
|
assert_eq!(
|
|
file_patterns.matches_path(path, kind),
|
|
match expected {
|
|
FilePatternsMatch::Passed
|
|
| FilePatternsMatch::PassedOptedOutExclude => true,
|
|
FilePatternsMatch::Excluded => false,
|
|
}
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_include() {
|
|
let cwd = current_dir();
|
|
// include is a closed set
|
|
let file_patterns = FilePatterns {
|
|
base: cwd.clone(),
|
|
include: Some(PathOrPatternSet(vec![
|
|
PathOrPattern::from_relative(&cwd, "target").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "other/**/*.ts").unwrap(),
|
|
])),
|
|
exclude: PathOrPatternSet(vec![]),
|
|
};
|
|
let run_test =
|
|
|path: &Path, kind: PathKind, expected: FilePatternsMatch| {
|
|
run_file_patterns_match_test(&file_patterns, path, kind, expected);
|
|
};
|
|
run_test(&cwd, PathKind::Directory, FilePatternsMatch::Passed);
|
|
run_test(
|
|
&cwd.join("other"),
|
|
PathKind::Directory,
|
|
FilePatternsMatch::Passed,
|
|
);
|
|
run_test(
|
|
&cwd.join("other/sub_dir"),
|
|
PathKind::Directory,
|
|
FilePatternsMatch::Passed,
|
|
);
|
|
run_test(
|
|
&cwd.join("not_matched"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
run_test(
|
|
&cwd.join("other/test.ts"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Passed,
|
|
);
|
|
run_test(
|
|
&cwd.join("other/test.js"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_exclude() {
|
|
let cwd = current_dir();
|
|
let file_patterns = FilePatterns {
|
|
base: cwd.clone(),
|
|
include: None,
|
|
exclude: PathOrPatternSet(vec![
|
|
PathOrPattern::from_relative(&cwd, "target").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "!not_excluded").unwrap(),
|
|
// lower items take priority
|
|
PathOrPattern::from_relative(&cwd, "excluded_then_not_excluded")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "!excluded_then_not_excluded")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "!not_excluded_then_excluded")
|
|
.unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "not_excluded_then_excluded")
|
|
.unwrap(),
|
|
]),
|
|
};
|
|
let run_test =
|
|
|path: &Path, kind: PathKind, expected: FilePatternsMatch| {
|
|
run_file_patterns_match_test(&file_patterns, path, kind, expected);
|
|
};
|
|
run_test(&cwd, PathKind::Directory, FilePatternsMatch::Passed);
|
|
run_test(
|
|
&cwd.join("target"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
run_test(
|
|
&cwd.join("not_excluded"),
|
|
PathKind::File,
|
|
FilePatternsMatch::PassedOptedOutExclude,
|
|
);
|
|
run_test(
|
|
&cwd.join("excluded_then_not_excluded"),
|
|
PathKind::File,
|
|
FilePatternsMatch::PassedOptedOutExclude,
|
|
);
|
|
run_test(
|
|
&cwd.join("not_excluded_then_excluded"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_include_exclude() {
|
|
let cwd = current_dir();
|
|
let file_patterns = FilePatterns {
|
|
base: cwd.clone(),
|
|
include: Some(PathOrPatternSet(vec![
|
|
PathOrPattern::from_relative(&cwd, "other").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "target").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "**/*.js").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "**/file.ts").unwrap(),
|
|
])),
|
|
exclude: PathOrPatternSet(vec![
|
|
PathOrPattern::from_relative(&cwd, "target").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "!target/unexcluded/").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "!target/other/**").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "**/*.ts").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "!**/file.ts").unwrap(),
|
|
]),
|
|
};
|
|
let run_test =
|
|
|path: &Path, kind: PathKind, expected: FilePatternsMatch| {
|
|
run_file_patterns_match_test(&file_patterns, path, kind, expected);
|
|
};
|
|
// matches other
|
|
run_test(
|
|
&cwd.join("other/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Passed,
|
|
);
|
|
// matches **/*.js
|
|
run_test(
|
|
&cwd.join("sub_dir/test.js"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Passed,
|
|
);
|
|
// not in include set
|
|
run_test(
|
|
&cwd.join("sub_dir/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
// .ts extension not matched
|
|
run_test(
|
|
&cwd.join("other/test.ts"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
// file.ts excluded from excludes
|
|
run_test(
|
|
&cwd.join("other/file.ts"),
|
|
PathKind::File,
|
|
FilePatternsMatch::PassedOptedOutExclude,
|
|
);
|
|
// not allowed target dir
|
|
run_test(
|
|
&cwd.join("target/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
run_test(
|
|
&cwd.join("target/sub_dir/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
// but allowed target/other dir
|
|
run_test(
|
|
&cwd.join("target/other/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::PassedOptedOutExclude,
|
|
);
|
|
run_test(
|
|
&cwd.join("target/other/sub/dir/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::PassedOptedOutExclude,
|
|
);
|
|
// and in target/unexcluded
|
|
run_test(
|
|
&cwd.join("target/unexcluded/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::PassedOptedOutExclude,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_include_excluded() {
|
|
let cwd = current_dir();
|
|
let file_patterns = FilePatterns {
|
|
base: cwd.clone(),
|
|
include: None,
|
|
exclude: PathOrPatternSet(vec![
|
|
PathOrPattern::from_relative(&cwd, "js/").unwrap(),
|
|
PathOrPattern::from_relative(&cwd, "!js/sub_dir/").unwrap(),
|
|
]),
|
|
};
|
|
let run_test =
|
|
|path: &Path, kind: PathKind, expected: FilePatternsMatch| {
|
|
run_file_patterns_match_test(&file_patterns, path, kind, expected);
|
|
};
|
|
run_test(
|
|
&cwd.join("js/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
run_test(
|
|
&cwd.join("js/sub_dir/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::PassedOptedOutExclude,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn file_patterns_opposite_incorrect_excluded_include() {
|
|
let cwd = current_dir();
|
|
let file_patterns = FilePatterns {
|
|
base: cwd.clone(),
|
|
include: None,
|
|
exclude: PathOrPatternSet(vec![
|
|
// this is lower priority
|
|
PathOrPattern::from_relative(&cwd, "!js/sub_dir/").unwrap(),
|
|
// this wins because it's higher priority
|
|
PathOrPattern::from_relative(&cwd, "js/").unwrap(),
|
|
]),
|
|
};
|
|
let run_test =
|
|
|path: &Path, kind: PathKind, expected: FilePatternsMatch| {
|
|
run_file_patterns_match_test(&file_patterns, path, kind, expected);
|
|
};
|
|
run_test(
|
|
&cwd.join("js/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
run_test(
|
|
&cwd.join("js/sub_dir/test.txt"),
|
|
PathKind::File,
|
|
FilePatternsMatch::Excluded,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn from_relative() {
|
|
let cwd = current_dir();
|
|
// leading dot slash
|
|
{
|
|
let pattern = PathOrPattern::from_relative(&cwd, "./**/*.ts").unwrap();
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("dir/foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("dir/foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
}
|
|
// no leading dot slash
|
|
{
|
|
let pattern = PathOrPattern::from_relative(&cwd, "**/*.ts").unwrap();
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("dir/foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("dir/foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
}
|
|
// exact file, leading dot slash
|
|
{
|
|
let pattern = PathOrPattern::from_relative(&cwd, "./foo.ts").unwrap();
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("dir/foo.ts")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
}
|
|
// exact file, no leading dot slash
|
|
{
|
|
let pattern = PathOrPattern::from_relative(&cwd, "foo.ts").unwrap();
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("dir/foo.ts")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
}
|
|
// error for invalid url
|
|
{
|
|
let err = PathOrPattern::from_relative(&cwd, "https://raw.githubusercontent.com%2Fdyedgreen%2Fdeno-sqlite%2Frework_api%2Fmod.ts").unwrap_err();
|
|
assert_eq!(
|
|
format!("{:#}", err),
|
|
"Invalid URL 'https://raw.githubusercontent.com%2Fdyedgreen%2Fdeno-sqlite%2Frework_api%2Fmod.ts'"
|
|
);
|
|
assert_eq!(
|
|
format!("{:#}", err.source().unwrap()),
|
|
"invalid international domain name"
|
|
);
|
|
}
|
|
// sibling dir
|
|
{
|
|
let pattern = PathOrPattern::from_relative(&cwd, "../sibling").unwrap();
|
|
let parent_dir = cwd.parent().unwrap();
|
|
assert_eq!(pattern.base_path().unwrap(), parent_dir.join("sibling"));
|
|
assert_eq!(
|
|
pattern.matches_path(&parent_dir.join("sibling/foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&parent_dir.join("./other/foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn from_relative_dot_slash() {
|
|
let cwd = current_dir();
|
|
let pattern = PathOrPattern::from_relative(&cwd, "./").unwrap();
|
|
match pattern {
|
|
PathOrPattern::Path(p) => assert_eq!(p, cwd),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn new_ctor() {
|
|
let cwd = current_dir();
|
|
for scheme in &["http", "https"] {
|
|
let url = format!("{}://deno.land/x/test", scheme);
|
|
let pattern = PathOrPattern::new(&url).unwrap();
|
|
match pattern {
|
|
PathOrPattern::RemoteUrl(p) => {
|
|
assert_eq!(p.as_str(), url)
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
for scheme in &["npm", "jsr"] {
|
|
let url = format!("{}:@denotest/basic", scheme);
|
|
let pattern = PathOrPattern::new(&url).unwrap();
|
|
match pattern {
|
|
PathOrPattern::RemoteUrl(p) => {
|
|
assert_eq!(p.as_str(), url)
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
{
|
|
let file_specifier = url_from_directory_path(&cwd).unwrap();
|
|
let pattern = PathOrPattern::new(file_specifier.as_str()).unwrap();
|
|
match pattern {
|
|
PathOrPattern::Path(p) => {
|
|
assert_eq!(p, cwd);
|
|
}
|
|
_ => {
|
|
unreachable!()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn from_relative_specifier() {
|
|
let cwd = current_dir();
|
|
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)
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
for scheme in &["npm", "jsr"] {
|
|
let url = format!("{}:@denotest/basic", scheme);
|
|
let pattern = PathOrPattern::from_relative(&cwd, &url).unwrap();
|
|
match pattern {
|
|
PathOrPattern::RemoteUrl(p) => {
|
|
assert_eq!(p.as_str(), url)
|
|
}
|
|
_ => 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);
|
|
}
|
|
_ => {
|
|
unreachable!()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn negated_globs() {
|
|
#[allow(clippy::disallowed_methods)]
|
|
let cwd = current_dir();
|
|
{
|
|
let pattern = GlobPattern::from_relative(&cwd, "!./**/*.ts").unwrap();
|
|
assert!(pattern.is_negated());
|
|
assert_eq!(pattern.base_path(), cwd);
|
|
assert!(pattern.as_str().starts_with('!'));
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.ts")),
|
|
PathGlobMatch::MatchedNegated
|
|
);
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.js")),
|
|
PathGlobMatch::NotMatched
|
|
);
|
|
let pattern = pattern.as_negated();
|
|
assert!(!pattern.is_negated());
|
|
assert_eq!(pattern.base_path(), cwd);
|
|
assert!(!pattern.as_str().starts_with('!'));
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.ts")),
|
|
PathGlobMatch::Matched
|
|
);
|
|
let pattern = pattern.as_negated();
|
|
assert!(pattern.is_negated());
|
|
assert_eq!(pattern.base_path(), cwd);
|
|
assert!(pattern.as_str().starts_with('!'));
|
|
assert_eq!(
|
|
pattern.matches_path(&cwd.join("foo.ts")),
|
|
PathGlobMatch::MatchedNegated
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_glob_pattern() {
|
|
assert!(!is_glob_pattern("npm:@scope/pkg@*"));
|
|
assert!(!is_glob_pattern("jsr:@scope/pkg@*"));
|
|
assert!(!is_glob_pattern("https://deno.land/x/?"));
|
|
assert!(!is_glob_pattern("http://deno.land/x/?"));
|
|
assert!(!is_glob_pattern("file:///deno.land/x/?"));
|
|
assert!(is_glob_pattern("**/*.ts"));
|
|
assert!(is_glob_pattern("test/?"));
|
|
assert!(!is_glob_pattern("test/test"));
|
|
}
|
|
|
|
fn current_dir() -> PathBuf {
|
|
// ok because this is test code
|
|
#[allow(clippy::disallowed_methods)]
|
|
std::env::current_dir().unwrap()
|
|
}
|
|
}
|