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-build",
|
||||
"napi-derive",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
|
||||
@ -21,6 +21,7 @@ napi = { version = '2.12.6', default-features = false, features = [
|
||||
'tokio_rt',
|
||||
] }
|
||||
napi-derive = '2.9.3'
|
||||
nom = '7.1.3'
|
||||
regex = "1.9.1"
|
||||
rayon = "1.7.0"
|
||||
thiserror = "1.0.40"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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::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 once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::fmt::Debug;
|
||||
use std::path::Path;
|
||||
use tracing::trace;
|
||||
@ -69,136 +72,25 @@ impl NxGlobSet {
|
||||
pub(crate) fn build_glob_set<S: AsRef<str> + Debug>(globs: &[S]) -> anyhow::Result<NxGlobSet> {
|
||||
let result = globs
|
||||
.iter()
|
||||
.map(|s| convert_glob(s.as_ref()))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.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()
|
||||
.map(|s| {
|
||||
let glob = s.as_ref();
|
||||
if glob.contains('!') || glob.contains('|') || glob.contains('(') {
|
||||
convert_glob(glob)
|
||||
} else {
|
||||
"*".to_string()
|
||||
}
|
||||
}
|
||||
None => "".to_string(),
|
||||
Ok(vec![glob.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}/**")
|
||||
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)
|
||||
NxGlobSetBuilder::new(&result)?.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
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]
|
||||
fn should_work_with_simple_globs() {
|
||||
@ -275,6 +167,7 @@ mod test {
|
||||
// matches
|
||||
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/doublenested/triplenested/file.txt"));
|
||||
// no matches
|
||||
assert!(!glob_set.is_match("dist/file.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-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 glob;
|
||||
pub mod hasher;
|
||||
mod logger;
|
||||
pub mod plugins;
|
||||
|
||||
@ -665,7 +665,7 @@ fn find_imports(
|
||||
#[cfg(test)]
|
||||
mod find_imports {
|
||||
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::walker::nx_walker;
|
||||
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::utils::glob::{build_glob_set, NxGlobSet};
|
||||
use hashbrown::HashSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
mod find_matching_projects;
|
||||
pub mod glob;
|
||||
pub mod path;
|
||||
|
||||
pub use find_matching_projects::*;
|
||||
|
||||
@ -5,7 +5,7 @@ use std::thread::available_parallelism;
|
||||
use crossbeam_channel::{unbounded, Receiver};
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
use crate::native::utils::glob::build_glob_set;
|
||||
use crate::native::glob::build_glob_set;
|
||||
|
||||
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 std::collections::HashMap;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user