feat(core): extglob to standard glob parser (#20089)
This commit is contained in:
parent
a73e9fd562
commit
dc7c3dbfe4
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1352,6 +1352,7 @@ dependencies = [
|
|||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
"napi-derive",
|
"napi-derive",
|
||||||
|
"nom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
|||||||
@ -21,6 +21,7 @@ napi = { version = '2.12.6', default-features = false, features = [
|
|||||||
'tokio_rt',
|
'tokio_rt',
|
||||||
] }
|
] }
|
||||||
napi-derive = '2.9.3'
|
napi-derive = '2.9.3'
|
||||||
|
nom = '7.1.3'
|
||||||
regex = "1.9.1"
|
regex = "1.9.1"
|
||||||
rayon = "1.7.0"
|
rayon = "1.7.0"
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::native::utils::glob::build_glob_set;
|
use crate::native::glob::build_glob_set;
|
||||||
use crate::native::utils::path::Normalize;
|
use crate::native::utils::path::Normalize;
|
||||||
use crate::native::walker::nx_walker_sync;
|
use crate::native::walker::nx_walker_sync;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
mod glob_group;
|
||||||
|
mod glob_parser;
|
||||||
|
mod glob_transform;
|
||||||
|
|
||||||
|
use crate::native::glob::glob_transform::convert_glob;
|
||||||
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
|
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
@ -69,136 +72,25 @@ impl NxGlobSet {
|
|||||||
pub(crate) fn build_glob_set<S: AsRef<str> + Debug>(globs: &[S]) -> anyhow::Result<NxGlobSet> {
|
pub(crate) fn build_glob_set<S: AsRef<str> + Debug>(globs: &[S]) -> anyhow::Result<NxGlobSet> {
|
||||||
let result = globs
|
let result = globs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| convert_glob(s.as_ref()))
|
.map(|s| {
|
||||||
.collect::<anyhow::Result<Vec<_>>>()?
|
let glob = s.as_ref();
|
||||||
.into_iter()
|
if glob.contains('!') || glob.contains('|') || glob.contains('(') {
|
||||||
.flatten()
|
convert_glob(glob)
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
trace!(?globs, ?result, "converted globs to result");
|
|
||||||
|
|
||||||
NxGlobSetBuilder::new(&result)?.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
// path/!{cache}/**
|
|
||||||
static NEGATIVE_DIR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"!\{(.*?)}").unwrap());
|
|
||||||
// path/**/(subdir1|subdir2)/*.(js|ts)
|
|
||||||
static GROUP_PATTERNS_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\((.*?)\)").unwrap());
|
|
||||||
// path/{cache}*
|
|
||||||
static SINGLE_PATTERN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\{(.*)}\*").unwrap());
|
|
||||||
|
|
||||||
/// Converts a glob string to a list of globs
|
|
||||||
/// e.g. `path/!(cache)/**` -> `path/**`, `!path/cache/**`
|
|
||||||
fn convert_glob(glob: &str) -> anyhow::Result<Vec<String>> {
|
|
||||||
// If there are no negations or multiple patterns, return the glob as is
|
|
||||||
if !glob.contains('!') && !glob.contains('|') && !glob.contains('(') {
|
|
||||||
return Ok(vec![glob.to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let glob = GROUP_PATTERNS_REGEX.replace_all(glob, |caps: ®ex::Captures| {
|
|
||||||
format!("{{{}}}", &caps[1].replace('|', ","))
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut globs: Vec<String> = Vec::new();
|
|
||||||
|
|
||||||
// push a glob directory glob that is either "path/*" or "path/**"
|
|
||||||
globs.push(
|
|
||||||
NEGATIVE_DIR_REGEX
|
|
||||||
.replace_all(&glob, |caps: ®ex::Captures| {
|
|
||||||
let capture = caps.get(0);
|
|
||||||
match capture {
|
|
||||||
Some(capture) => {
|
|
||||||
let char = glob.as_bytes()[capture.end()] as char;
|
|
||||||
if char == '*' {
|
|
||||||
"".to_string()
|
|
||||||
} else {
|
} else {
|
||||||
"*".to_string()
|
Ok(vec![glob.to_string()])
|
||||||
}
|
|
||||||
}
|
|
||||||
None => "".to_string(),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into(),
|
.collect::<anyhow::Result<Vec<_>>>()?
|
||||||
);
|
.concat();
|
||||||
|
|
||||||
let matches: Vec<_> = NEGATIVE_DIR_REGEX.find_iter(&glob).collect();
|
trace!(?globs, ?result, "converted globs");
|
||||||
|
|
||||||
// convert negative captures to globs (e.g. "path/!{cache,dir}/**" -> "!path/{cache,dir}/**")
|
NxGlobSetBuilder::new(&result)?.build()
|
||||||
if matches.len() == 1 {
|
|
||||||
globs.push(format!(
|
|
||||||
"!{}",
|
|
||||||
SINGLE_PATTERN_REGEX
|
|
||||||
.replace(&glob, |caps: ®ex::Captures| { format!("{}*", &caps[1]) })
|
|
||||||
.replace('!', "")
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
// if there is more than one negative capture, convert each capture to a *, and negate the whole glob
|
|
||||||
for matched in matches {
|
|
||||||
let a = glob.replace(matched.as_str(), "*");
|
|
||||||
globs.push(format!("!{}", a.replace('!', "")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(globs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_globs_full_convert() {
|
|
||||||
let full_convert =
|
|
||||||
convert_glob("dist/!(cache|cache2)/**/!(README|LICENSE).(js|ts)").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
full_convert,
|
|
||||||
[
|
|
||||||
"dist/*/**/*.{js,ts}",
|
|
||||||
"!dist/*/**/{README,LICENSE}.{js,ts}",
|
|
||||||
"!dist/{cache,cache2}/**/*.{js,ts}",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_globs_no_dirs() {
|
|
||||||
let no_dirs = convert_glob("dist/**/!(README|LICENSE).(js|ts)").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
no_dirs,
|
|
||||||
["dist/**/*.{js,ts}", "!dist/**/{README,LICENSE}.{js,ts}"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_globs_no_files() {
|
|
||||||
let no_files = convert_glob("dist/!(cache|cache2)/**/*.(js|ts)").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
no_files,
|
|
||||||
["dist/*/**/*.{js,ts}", "!dist/{cache,cache2}/**/*.{js,ts}"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_globs_no_extensions() {
|
|
||||||
let no_extensions = convert_glob("dist/!(cache|cache2)/**/*.js").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
no_extensions,
|
|
||||||
["dist/*/**/*.js", "!dist/{cache,cache2}/**/*.js"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_globs_no_patterns() {
|
|
||||||
let no_patterns = convert_glob("dist/**/*.js").unwrap();
|
|
||||||
assert_eq!(no_patterns, ["dist/**/*.js",]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_globs_single_negative() {
|
|
||||||
let negative_single_dir = convert_glob("packages/!(package-a)*").unwrap();
|
|
||||||
assert_eq!(negative_single_dir, ["packages/*", "!packages/package-a*"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_work_with_simple_globs() {
|
fn should_work_with_simple_globs() {
|
||||||
@ -275,6 +167,7 @@ mod test {
|
|||||||
// matches
|
// matches
|
||||||
assert!(glob_set.is_match("dist/nested/file.txt"));
|
assert!(glob_set.is_match("dist/nested/file.txt"));
|
||||||
assert!(glob_set.is_match("dist/nested/file.md"));
|
assert!(glob_set.is_match("dist/nested/file.md"));
|
||||||
|
assert!(glob_set.is_match("dist/nested/doublenested/triplenested/file.txt"));
|
||||||
// no matches
|
// no matches
|
||||||
assert!(!glob_set.is_match("dist/file.txt"));
|
assert!(!glob_set.is_match("dist/file.txt"));
|
||||||
assert!(!glob_set.is_match("dist/cache/nested/README.txt"));
|
assert!(!glob_set.is_match("dist/cache/nested/README.txt"));
|
||||||
@ -322,4 +215,72 @@ mod test {
|
|||||||
assert!(!glob_set.is_match("packages/package-a-b/nested"));
|
assert!(!glob_set.is_match("packages/package-a-b/nested"));
|
||||||
assert!(!glob_set.is_match("packages/package-b/nested"));
|
assert!(!glob_set.is_match("packages/package-b/nested"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_handle_complex_extglob_patterns() {
|
||||||
|
let glob_set = build_glob_set(&["**/?(*.)+(spec|test).[jt]s?(x)?(.snap)"]).unwrap();
|
||||||
|
// matches
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.jsx.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.js.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.jsx"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.js"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.tsx.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.ts.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.tsx"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/spec.ts"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.jsx.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.js.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.jsx"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.js"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.tsx.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.ts.snap"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.tsx"));
|
||||||
|
assert!(glob_set.is_match("packages/package-a/file.spec.ts"));
|
||||||
|
assert!(glob_set.is_match("spec.jsx.snap"));
|
||||||
|
assert!(glob_set.is_match("spec.js.snap"));
|
||||||
|
assert!(glob_set.is_match("spec.jsx"));
|
||||||
|
assert!(glob_set.is_match("spec.js"));
|
||||||
|
assert!(glob_set.is_match("spec.tsx.snap"));
|
||||||
|
assert!(glob_set.is_match("spec.ts.snap"));
|
||||||
|
assert!(glob_set.is_match("spec.tsx"));
|
||||||
|
assert!(glob_set.is_match("spec.ts"));
|
||||||
|
assert!(glob_set.is_match("file.spec.jsx.snap"));
|
||||||
|
assert!(glob_set.is_match("file.spec.js.snap"));
|
||||||
|
assert!(glob_set.is_match("file.spec.jsx"));
|
||||||
|
assert!(glob_set.is_match("file.spec.js"));
|
||||||
|
assert!(glob_set.is_match("file.spec.tsx.snap"));
|
||||||
|
assert!(glob_set.is_match("file.spec.ts.snap"));
|
||||||
|
assert!(glob_set.is_match("file.spec.tsx"));
|
||||||
|
assert!(glob_set.is_match("file.spec.ts"));
|
||||||
|
|
||||||
|
// no matches
|
||||||
|
assert!(!glob_set.is_match("packages/package-a/spec.jsx.snapx"));
|
||||||
|
assert!(!glob_set.is_match("packages/package-a/spec.js.snapx"));
|
||||||
|
assert!(!glob_set.is_match("packages/package-a/file.ts"));
|
||||||
|
|
||||||
|
let glob_set = build_glob_set(&["**/!(*.module).ts"]).unwrap();
|
||||||
|
//matches
|
||||||
|
assert!(glob_set.is_match("test.ts"));
|
||||||
|
assert!(glob_set.is_match("nested/comp.test.ts"));
|
||||||
|
//no matches
|
||||||
|
assert!(!glob_set.is_match("test.module.ts"));
|
||||||
|
|
||||||
|
let glob_set = build_glob_set(&["**/*.*(component,module).ts?(x)"]).unwrap();
|
||||||
|
//matches
|
||||||
|
assert!(glob_set.is_match("test.component.ts"));
|
||||||
|
assert!(glob_set.is_match("test.module.ts"));
|
||||||
|
assert!(glob_set.is_match("test.component.tsx"));
|
||||||
|
assert!(glob_set.is_match("test.module.tsx"));
|
||||||
|
assert!(glob_set.is_match("nested/comp.test.component.ts"));
|
||||||
|
assert!(glob_set.is_match("nested/comp.test.module.ts"));
|
||||||
|
assert!(glob_set.is_match("nested/comp.test.component.tsx"));
|
||||||
|
assert!(glob_set.is_match("nested/comp.test.module.tsx"));
|
||||||
|
//no matches
|
||||||
|
assert!(!glob_set.is_match("test.ts"));
|
||||||
|
assert!(!glob_set.is_match("test.component.spec.ts"));
|
||||||
|
assert!(!glob_set.is_match("test.module.spec.ts"));
|
||||||
|
assert!(!glob_set.is_match("test.component.spec.tsx"));
|
||||||
|
assert!(!glob_set.is_match("test.module.spec.tsx"));
|
||||||
|
assert!(!glob_set.is_match("nested/comp.test.component.spec.ts"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
46
packages/nx/src/native/glob/glob_group.rs
Normal file
46
packages/nx/src/native/glob/glob_group.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum GlobGroup<'a> {
|
||||||
|
// *(a|b|c)
|
||||||
|
ZeroOrMore(Cow<'a, str>),
|
||||||
|
// ?(a|b|c)
|
||||||
|
ZeroOrOne(Cow<'a, str>),
|
||||||
|
// +(a|b|c)
|
||||||
|
OneOrMore(Cow<'a, str>),
|
||||||
|
// @(a|b|c)
|
||||||
|
ExactOne(Cow<'a, str>),
|
||||||
|
// !(a|b|c)
|
||||||
|
Negated(Cow<'a, str>),
|
||||||
|
NegatedFileName(Cow<'a, str>),
|
||||||
|
NonSpecialGroup(Cow<'a, str>),
|
||||||
|
NonSpecial(Cow<'a, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for GlobGroup<'a> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
GlobGroup::ZeroOrMore(s)
|
||||||
|
| GlobGroup::ZeroOrOne(s)
|
||||||
|
| GlobGroup::OneOrMore(s)
|
||||||
|
| GlobGroup::ExactOne(s)
|
||||||
|
| GlobGroup::NonSpecialGroup(s)
|
||||||
|
| GlobGroup::Negated(s) => {
|
||||||
|
if s.contains(',') {
|
||||||
|
write!(f, "{{{}}}", s)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlobGroup::NegatedFileName(s) => {
|
||||||
|
if s.contains(',') {
|
||||||
|
write!(f, "{{{}}}.", s)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}.", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlobGroup::NonSpecial(s) => write!(f, "{}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
packages/nx/src/native/glob/glob_parser.rs
Normal file
276
packages/nx/src/native/glob/glob_parser.rs
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
use crate::native::glob::glob_group::GlobGroup;
|
||||||
|
use nom::branch::alt;
|
||||||
|
use nom::bytes::complete::{is_not, tag, take_till, take_until, take_while};
|
||||||
|
use nom::combinator::{eof, map, map_parser};
|
||||||
|
use nom::error::{context, convert_error, VerboseError};
|
||||||
|
use nom::multi::{many_till, separated_list0};
|
||||||
|
use nom::sequence::{preceded, terminated};
|
||||||
|
use nom::{Finish, IResult};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
fn simple_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"simple_group",
|
||||||
|
map(preceded(tag("("), group), GlobGroup::NonSpecialGroup),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zero_or_more_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"zero_or_more_group",
|
||||||
|
map(preceded(tag("*("), group), GlobGroup::ZeroOrMore),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zero_or_one_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"zero_or_one_group",
|
||||||
|
map(preceded(tag("?("), group), GlobGroup::ZeroOrOne),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn one_or_more_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"one_or_more_group",
|
||||||
|
map(preceded(tag("+("), group), GlobGroup::OneOrMore),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exact_one_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"exact_one_group",
|
||||||
|
map(preceded(tag("@("), group), GlobGroup::ExactOne),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn negated_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"negated_group",
|
||||||
|
map(preceded(tag("!("), group), GlobGroup::Negated),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn negated_file_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context("negated_file_group", |input| {
|
||||||
|
let (input, result) = preceded(tag("!("), group)(input)?;
|
||||||
|
let (input, _) = tag(".")(input)?;
|
||||||
|
Ok((input, GlobGroup::NegatedFileName(result)))
|
||||||
|
})(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_special_character(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"non_special_character",
|
||||||
|
map(
|
||||||
|
alt((
|
||||||
|
take_while(|c| c != '?' && c != '+' && c != '@' && c != '!' && c != '('),
|
||||||
|
is_not("*("),
|
||||||
|
)),
|
||||||
|
|i: &str| GlobGroup::NonSpecial(i.into()),
|
||||||
|
),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn group(input: &str) -> IResult<&str, Cow<str>, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"group",
|
||||||
|
map_parser(terminated(take_until(")"), tag(")")), separated_group_items),
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn separated_group_items(input: &str) -> IResult<&str, Cow<str>, VerboseError<&str>> {
|
||||||
|
map(
|
||||||
|
separated_list0(
|
||||||
|
alt((tag("|"), tag(","))),
|
||||||
|
take_while(|c| c != '|' && c != ','),
|
||||||
|
),
|
||||||
|
|items: Vec<&str>| {
|
||||||
|
if items.len() == 1 {
|
||||||
|
Cow::from(items[0])
|
||||||
|
} else {
|
||||||
|
Cow::from(items.join(","))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_segment(input: &str) -> IResult<&str, Vec<GlobGroup>, VerboseError<&str>> {
|
||||||
|
context(
|
||||||
|
"parse_segment",
|
||||||
|
many_till(
|
||||||
|
context(
|
||||||
|
"glob_group",
|
||||||
|
alt((
|
||||||
|
simple_group,
|
||||||
|
zero_or_more_group,
|
||||||
|
zero_or_one_group,
|
||||||
|
one_or_more_group,
|
||||||
|
exact_one_group,
|
||||||
|
negated_file_group,
|
||||||
|
negated_group,
|
||||||
|
non_special_character,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
eof,
|
||||||
|
),
|
||||||
|
)(input)
|
||||||
|
.map(|(i, (groups, _))| (i, groups))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn separated_segments(input: &str) -> IResult<&str, Vec<Vec<GlobGroup>>, VerboseError<&str>> {
|
||||||
|
separated_list0(tag("/"), map_parser(take_till(|c| c == '/'), parse_segment))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// match on !test/, but not !(test)/
|
||||||
|
fn negated_glob(input: &str) -> (&str, bool) {
|
||||||
|
let (tagged_input, _) = match tag::<_, _, VerboseError<&str>>("!")(input) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => return (input, false),
|
||||||
|
};
|
||||||
|
|
||||||
|
match tag::<_, _, VerboseError<&str>>("(")(tagged_input) {
|
||||||
|
Ok(_) => (input, false),
|
||||||
|
Err(_) => (tagged_input, true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_glob(input: &str) -> anyhow::Result<(bool, Vec<Vec<GlobGroup>>)> {
|
||||||
|
let (input, negated) = negated_glob(input);
|
||||||
|
let result = separated_segments(input).finish();
|
||||||
|
if let Ok((_, result)) = result {
|
||||||
|
Ok((negated, result))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"{}",
|
||||||
|
convert_error(input, result.err().unwrap())
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::native::glob::glob_group::GlobGroup;
|
||||||
|
use crate::native::glob::glob_parser::parse_glob;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_parse_globs() {
|
||||||
|
let result = parse_glob("a/b/c").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::NonSpecial("a".into())],
|
||||||
|
vec![GlobGroup::NonSpecial("b".into())],
|
||||||
|
vec![GlobGroup::NonSpecial("c".into())]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = parse_glob("a/*.ts").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::NonSpecial("a".into())],
|
||||||
|
vec![GlobGroup::NonSpecial("*.ts".into())]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = parse_glob("a/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::NonSpecial("a".into())],
|
||||||
|
vec![GlobGroup::NonSpecial("**".into()),],
|
||||||
|
vec![
|
||||||
|
GlobGroup::ZeroOrOne("*.".into()),
|
||||||
|
GlobGroup::OneOrMore("spec,test".into()),
|
||||||
|
GlobGroup::NonSpecial(".[jt]s".into()),
|
||||||
|
GlobGroup::ZeroOrOne("x".into()),
|
||||||
|
GlobGroup::ZeroOrOne(".snap".into())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = parse_glob("!(e2e|test)/*.ts").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::Negated("e2e,test".into())],
|
||||||
|
vec![GlobGroup::NonSpecial("*.ts".into())]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = parse_glob("**/*.(js|ts)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::NonSpecial("**".into())],
|
||||||
|
vec![
|
||||||
|
GlobGroup::NonSpecial("*.".into()),
|
||||||
|
GlobGroup::NonSpecialGroup("js,ts".into())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = parse_glob("**/!(README).[jt]s!(x)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::NonSpecial("**".into())],
|
||||||
|
vec![
|
||||||
|
GlobGroup::NegatedFileName("README".into()),
|
||||||
|
GlobGroup::NonSpecial("[jt]s".into()),
|
||||||
|
GlobGroup::Negated("x".into())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = parse_glob("!test/!(README).[jt]s!(x)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::NonSpecial("test".into())],
|
||||||
|
vec![
|
||||||
|
GlobGroup::NegatedFileName("README".into()),
|
||||||
|
GlobGroup::NonSpecial("[jt]s".into()),
|
||||||
|
GlobGroup::Negated("x".into())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = parse_glob("!(test)/!(README).[jt]s!(x)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
vec![
|
||||||
|
vec![GlobGroup::Negated("test".into())],
|
||||||
|
vec![
|
||||||
|
GlobGroup::NegatedFileName("README".into()),
|
||||||
|
GlobGroup::NonSpecial("[jt]s".into()),
|
||||||
|
GlobGroup::Negated("x".into())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
187
packages/nx/src/native/glob/glob_transform.rs
Normal file
187
packages/nx/src/native/glob/glob_transform.rs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
use crate::native::glob::glob_group::GlobGroup;
|
||||||
|
use crate::native::glob::glob_parser::parse_glob;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum GlobType {
|
||||||
|
Negative(String),
|
||||||
|
Positive(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_glob(glob: &str) -> anyhow::Result<Vec<String>> {
|
||||||
|
let (negated, parsed) = parse_glob(glob)?;
|
||||||
|
let mut built_segments: Vec<Vec<GlobType>> = Vec::new();
|
||||||
|
for (index, glob_segment) in parsed.iter().enumerate() {
|
||||||
|
let is_last = index == parsed.len() - 1;
|
||||||
|
built_segments.push(build_segment("", glob_segment, is_last, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut globs = built_segments
|
||||||
|
.iter()
|
||||||
|
.multi_cartesian_product()
|
||||||
|
.map(|product| {
|
||||||
|
let mut negative = false;
|
||||||
|
let mut full_path = false;
|
||||||
|
let mut path = String::from("");
|
||||||
|
for (index, glob) in product.iter().enumerate() {
|
||||||
|
full_path = index == product.len() - 1;
|
||||||
|
match glob {
|
||||||
|
GlobType::Negative(s) if index != product.len() - 1 => {
|
||||||
|
path.push_str(&format!("{}/", s));
|
||||||
|
negative = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
GlobType::Negative(s) => {
|
||||||
|
path.push_str(&format!("{}/", s));
|
||||||
|
negative = true;
|
||||||
|
}
|
||||||
|
GlobType::Positive(s) => {
|
||||||
|
path.push_str(&format!("{}/", s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified_path = if full_path {
|
||||||
|
&path[..path.len() - 1]
|
||||||
|
} else {
|
||||||
|
&path
|
||||||
|
};
|
||||||
|
|
||||||
|
if negative || negated {
|
||||||
|
format!("!{}", modified_path)
|
||||||
|
} else {
|
||||||
|
modified_path.to_owned()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
globs.sort();
|
||||||
|
Ok(globs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_segment(
|
||||||
|
existing: &str,
|
||||||
|
group: &[GlobGroup],
|
||||||
|
is_last_segment: bool,
|
||||||
|
is_negative: bool,
|
||||||
|
) -> Vec<GlobType> {
|
||||||
|
if let Some(glob_part) = group.iter().next() {
|
||||||
|
let built_glob = format!("{}{}", existing, glob_part);
|
||||||
|
match glob_part {
|
||||||
|
GlobGroup::ZeroOrMore(_) | GlobGroup::ZeroOrOne(_) => {
|
||||||
|
let existing = if !is_last_segment { "*" } else { existing };
|
||||||
|
let off_group = build_segment(existing, &group[1..], is_last_segment, is_negative);
|
||||||
|
let on_group =
|
||||||
|
build_segment(&built_glob, &group[1..], is_last_segment, is_negative);
|
||||||
|
off_group.into_iter().chain(on_group).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
GlobGroup::Negated(_) => {
|
||||||
|
let existing = if !is_last_segment { "*" } else { existing };
|
||||||
|
let off_group = build_segment(existing, &group[1..], is_last_segment, is_negative);
|
||||||
|
let on_group = build_segment(&built_glob, &group[1..], is_last_segment, true);
|
||||||
|
off_group.into_iter().chain(on_group).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
GlobGroup::NegatedFileName(_) => {
|
||||||
|
let off_group = build_segment("*.", &group[1..], is_last_segment, is_negative);
|
||||||
|
let on_group = build_segment(&built_glob, &group[1..], is_last_segment, true);
|
||||||
|
off_group.into_iter().chain(on_group).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
GlobGroup::OneOrMore(_)
|
||||||
|
| GlobGroup::ExactOne(_)
|
||||||
|
| GlobGroup::NonSpecial(_)
|
||||||
|
| GlobGroup::NonSpecialGroup(_) => {
|
||||||
|
build_segment(&built_glob, &group[1..], is_last_segment, is_negative)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if is_negative {
|
||||||
|
vec![GlobType::Negative(existing.to_string())]
|
||||||
|
} else {
|
||||||
|
vec![GlobType::Positive(existing.to_string())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::convert_glob;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_globs_full_convert() {
|
||||||
|
let full_convert =
|
||||||
|
convert_glob("dist/!(cache|cache2)/**/!(README|LICENSE).(js|ts)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
full_convert,
|
||||||
|
[
|
||||||
|
"!dist/*/**/{README,LICENSE}.{js,ts}",
|
||||||
|
"!dist/{cache,cache2}/",
|
||||||
|
"dist/*/**/*.{js,ts}",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_globs_no_dirs() {
|
||||||
|
let no_dirs = convert_glob("dist/**/!(README|LICENSE).(js|ts)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
no_dirs,
|
||||||
|
["!dist/**/{README,LICENSE}.{js,ts}", "dist/**/*.{js,ts}",]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_globs_no_files() {
|
||||||
|
let no_files = convert_glob("dist/!(cache|cache2)/**/*.(js|ts)").unwrap();
|
||||||
|
assert_eq!(no_files, ["!dist/{cache,cache2}/", "dist/*/**/*.{js,ts}",]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_globs_no_extensions() {
|
||||||
|
let no_extensions = convert_glob("dist/!(cache|cache2)/**/*.js").unwrap();
|
||||||
|
assert_eq!(no_extensions, ["!dist/{cache,cache2}/", "dist/*/**/*.js",]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_globs_no_patterns() {
|
||||||
|
let no_patterns = convert_glob("dist/**/*.js").unwrap();
|
||||||
|
assert_eq!(no_patterns, ["dist/**/*.js",]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_globs_single_negative() {
|
||||||
|
let negative_single_dir = convert_glob("packages/!(package-a)*").unwrap();
|
||||||
|
assert_eq!(negative_single_dir, ["!packages/package-a*", "packages/*"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transforming_globs() {
|
||||||
|
let globs = convert_glob("!(test|e2e)/?(*.)+(spec|test).[jt]s!(x)?(.snap)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
globs,
|
||||||
|
vec![
|
||||||
|
"!*/*.{spec,test}.[jt]sx",
|
||||||
|
"!*/*.{spec,test}.[jt]sx.snap",
|
||||||
|
"!*/{spec,test}.[jt]sx",
|
||||||
|
"!*/{spec,test}.[jt]sx.snap",
|
||||||
|
"!{test,e2e}/",
|
||||||
|
"*/*.{spec,test}.[jt]s",
|
||||||
|
"*/*.{spec,test}.[jt]s.snap",
|
||||||
|
"*/{spec,test}.[jt]s",
|
||||||
|
"*/{spec,test}.[jt]s.snap"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let globs = convert_glob("**/!(package-a)*").unwrap();
|
||||||
|
assert_eq!(globs, vec!["!**/package-a*", "**/*"]);
|
||||||
|
|
||||||
|
let globs = convert_glob("dist/!(cache|cache2)/**/!(README|LICENSE).(js|ts)").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
globs,
|
||||||
|
[
|
||||||
|
"!dist/*/**/{README,LICENSE}.{js,ts}",
|
||||||
|
"!dist/{cache,cache2}/",
|
||||||
|
"dist/*/**/*.{js,ts}"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod glob;
|
||||||
pub mod hasher;
|
pub mod hasher;
|
||||||
mod logger;
|
mod logger;
|
||||||
pub mod plugins;
|
pub mod plugins;
|
||||||
|
|||||||
@ -665,7 +665,7 @@ fn find_imports(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod find_imports {
|
mod find_imports {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::native::utils::glob::build_glob_set;
|
use crate::native::glob::build_glob_set;
|
||||||
use crate::native::utils::path::Normalize;
|
use crate::native::utils::path::Normalize;
|
||||||
use crate::native::walker::nx_walker;
|
use crate::native::walker::nx_walker;
|
||||||
use assert_fs::prelude::*;
|
use assert_fs::prelude::*;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
use crate::native::glob::{build_glob_set, NxGlobSet};
|
||||||
use crate::native::project_graph::types::{Project, ProjectGraph};
|
use crate::native::project_graph::types::{Project, ProjectGraph};
|
||||||
use crate::native::utils::glob::{build_glob_set, NxGlobSet};
|
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
mod find_matching_projects;
|
mod find_matching_projects;
|
||||||
pub mod glob;
|
|
||||||
pub mod path;
|
pub mod path;
|
||||||
|
|
||||||
pub use find_matching_projects::*;
|
pub use find_matching_projects::*;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::thread::available_parallelism;
|
|||||||
use crossbeam_channel::{unbounded, Receiver};
|
use crossbeam_channel::{unbounded, Receiver};
|
||||||
use ignore::WalkBuilder;
|
use ignore::WalkBuilder;
|
||||||
|
|
||||||
use crate::native::utils::glob::build_glob_set;
|
use crate::native::glob::build_glob_set;
|
||||||
|
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::native::utils::glob::build_glob_set;
|
use crate::native::glob::build_glob_set;
|
||||||
use crate::native::utils::path::Normalize;
|
use crate::native::utils::path::Normalize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user