use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
#[derive(Debug, Clone, Default)]
pub(crate) struct Globs {
include: GlobSet,
exclude: GlobSet,
has_include: bool,
also: Option<Box<Globs>>,
}
fn compile(pattern: &str) -> Vec<Glob> {
let pattern = pattern.trim().trim_start_matches("./").trim_end_matches('!');
if pattern.is_empty() {
return Vec::new();
}
skif_js_plugins::glob::expand_empty_alternatives(pattern)
.iter()
.filter_map(|variant| GlobBuilder::new(variant).literal_separator(true).build().ok())
.collect()
}
fn build(globs: Vec<Glob>) -> GlobSet {
let mut builder = GlobSetBuilder::new();
for glob in globs {
builder.add(glob);
}
builder.build().unwrap_or_default()
}
impl Globs {
pub(crate) fn new<I, S>(patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut include = Vec::new();
let mut exclude = Vec::new();
for pattern in patterns {
let pattern = pattern.as_ref();
if let Some(negated) = pattern.strip_prefix('!') {
exclude.extend(compile(negated));
} else {
include.extend(compile(pattern));
}
}
Self {
has_include: !include.is_empty(),
include: build(include),
exclude: build(exclude),
also: None,
}
}
#[must_use]
pub(crate) fn or(mut self, other: Self) -> Self {
if !other.is_empty() {
self.also = Some(Box::new(other));
}
self
}
pub(crate) fn is_empty(&self) -> bool {
!self.has_include && self.also.as_ref().is_none_or(|also| also.is_empty())
}
pub(crate) fn is_match(&self, path: &str) -> bool {
(self.has_include && self.include.is_match(path) && !self.exclude.is_match(path))
|| self.also.as_ref().is_some_and(|also| also.is_match(path))
}
pub(crate) fn matches_any(&self, path: &str) -> bool {
(self.has_include && self.include.is_match(path))
|| self.also.as_ref().is_some_and(|also| also.matches_any(path))
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Names {
set: GlobSet,
}
impl Names {
pub(crate) fn new<I, S>(patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let globs = patterns
.into_iter()
.flat_map(|pattern| skif_js_plugins::glob::expand_empty_alternatives(pattern.as_ref()))
.filter_map(|pattern| Glob::new(&pattern).ok())
.collect();
Self { set: build(globs) }
}
pub(crate) fn is_match(&self, name: &str) -> bool {
self.set.is_match(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn include_and_exclude() {
let globs = Globs::new(["**/*.ts", "!**/*.test.ts", "./src/**/*.{js,jsx}"]);
assert!(globs.is_match("src/a.ts"));
assert!(globs.is_match("a.ts"));
assert!(!globs.is_match("src/a.test.ts"));
assert!(globs.is_match("src/deep/b.jsx"));
assert!(!globs.is_match("lib/b.jsx"));
assert!(!Globs::new(["!a"]).is_match("a"));
}
#[test]
fn names() {
let names = Names::new(["@types/*", "eslint"]);
assert!(names.is_match("@types/node"));
assert!(names.is_match("eslint"));
assert!(!names.is_match("eslint-plugin-x"));
}
#[test]
fn single_star_stays_in_one_segment() {
let globs = Globs::new(["src/*.ts"]);
assert!(globs.is_match("src/a.ts"));
assert!(!globs.is_match("src/deep/a.ts"));
}
}