Skip to content

Commit

Permalink
Expose Variance etc. in a query module.
Browse files Browse the repository at this point in the history
  • Loading branch information
olson-sean-k committed Mar 16, 2024
1 parent e439b14 commit a2f8764
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 267 deletions.
180 changes: 36 additions & 144 deletions src/lib.rs
Expand Up @@ -34,6 +34,7 @@ mod capture;
mod diagnostics;
mod encode;
mod filter;
pub mod query;
mod rule;
mod token;
pub mod walk;
Expand Down Expand Up @@ -75,9 +76,11 @@ use std::str::{self, FromStr};
use thiserror::Error;

use crate::encode::CompileError;
use crate::query::{CapturingToken, When};
use crate::rule::{Checked, RuleError};
use crate::token::{
ConcatenationTree, ExpressionMetadata, ParseError, Text, Token, TokenTree, Tokenized,
ConcatenationTree, Depth, ExpressionMetadata, GlobVariance, ParseError, Text, Token, TokenTree,
Tokenized,
};
#[cfg(feature = "walk")]
use crate::walk::WalkError;
Expand Down Expand Up @@ -112,126 +115,6 @@ impl StrExt for str {
}
}

/// Token that captures matched text in a glob expression.
///
/// # Examples
///
/// `CapturingToken`s can be used to isolate sub-expressions.
///
/// ```rust
/// use wax::Glob;
///
/// let expression = "**/*.txt";
/// let glob = Glob::new(expression).unwrap();
/// for token in glob.captures() {
/// let (start, n) = token.span();
/// println!("capturing sub-expression: {}", &expression[start..][..n]);
/// }
/// ```
#[derive(Clone, Copy, Debug)]
pub struct CapturingToken {
index: usize,
span: Span,
}

impl CapturingToken {
/// Gets the index of the capture.
///
/// Captures are one-indexed and the index zero always represents the implicit capture of the
/// complete match, so the index of `CapturingToken`s is always one or greater. See
/// [`MatchedText`].
///
/// [`MatchedText`]: crate::MatchedText
pub fn index(&self) -> usize {
self.index
}

/// Gets the span of the token's sub-expression.
pub fn span(&self) -> Span {
self.span
}
}

// This type is similar to `token::Variance<InvariantText<'_>>`, but is simplified for the public
// API. Invariant text is always expressed as a path and no variant bounds are provided.
/// Variance of a [`Program`].
///
/// The variance of a pattern describes the kinds of paths it can match with respect to the
/// platform file system APIs. [`Program`]s are either variant or invariant.
///
/// An invariant [`Program`] can be represented and completely described by an equivalent path
/// using the platform's file system APIs. For example, the glob expression `path/to/file.txt`
/// resolves identically to the paths `path/to/file.txt` and `path\to\file.txt` on Unix and
/// Windows, respectively.
///
/// A variant [`Program`] resolves differently than any particular path used with the platform's
/// file system APIs. Such an expression cannot be represented by a single path. This is typically
/// because the expression matches multiple texts using a regular pattern, such as in the glob
/// expression `**/*.rs`.
///
/// [`Program`]: crate::Program
/// [`Variance`]: crate::Variance
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Variance {
/// A [`Program`] is invariant and equivalent to a path.
///
/// Some non-literal expressions may be invariant, such as in the expression
/// `path/[t][o]/{file,file}.txt`, which is invariant on Unix (but not on Windows, because the
/// character class expressions do not match with case folding).
///
/// [`Program`]: crate::Program
Invariant(
/// An equivalent path that completely describes the invariant [`Program`] with respect to
/// platform file system APIs.
///
/// [`Program`]: crate::Program
PathBuf,
),
/// A [`Program`] is variant and cannot be completely described by a path.
///
/// Variant expressions may be formed from literals or other **seemingly** invariant
/// expressions. For example, the variance of literals considers the case sensitivity of the
/// platform's file system APIs, so the expression `(?i)path/to/file.txt` is variant on Unix
/// but not on Windows. Similarly, the expression `path/[t][o]/file.txt` is variant on Windows
/// but not on Unix.
///
/// [`Program`]: crate::Program
Variant,
}

impl Variance {
/// Gets the equivalent native path if invariant.
///
/// Returns `None` if variant.
pub fn path(&self) -> Option<&Path> {
match self {
Variance::Invariant(ref path) => Some(path),
Variance::Variant => None,
}
}

/// Returns `true` if invariant.
pub fn is_invariant(&self) -> bool {
matches!(self, Variance::Invariant(_))
}

/// Returns `true` if variant.
pub fn is_variant(&self) -> bool {
matches!(self, Variance::Variant)
}
}

impl From<token::Variance<Text<'_>>> for Variance {
fn from(variance: token::Variance<Text<'_>>) -> Self {
match variance {
token::Variance::Invariant(text) => {
Variance::Invariant(PathBuf::from(text.to_string().into_owned()))
},
token::Variance::Variant(_) => Variance::Variant,
}
}
}

/// A representation of a glob expression.
///
/// This trait is implemented by types that can be converted into a [`Program`], such as `str`
Expand Down Expand Up @@ -291,11 +174,15 @@ pub trait Program<'t>: Pattern<'t, Error = Infallible> {
/// [`MatchedText`]: crate::MatchedText
fn matched<'p>(&self, path: &'p CandidatePath<'_>) -> Option<MatchedText<'p>>;

/// Gets the variance of the pattern.
fn depth(&self) -> GlobVariance<Depth>;

fn text(&self) -> GlobVariance<Text<'t>>;

/// Returns `true` if the glob has a root.
///
/// The variance of a pattern describes the kinds of paths it can match with respect to the
/// platform file system APIs.
fn variance(&self) -> Variance;
/// As with Unix paths, a glob expression has a root if it begins with a separator `/`.
/// Patterns other than separators may also root an expression, such as `/**` or `</root:1,>`.
fn has_root(&self) -> When;

/// Returns `true` if the pattern is exhaustive.
///
Expand Down Expand Up @@ -749,18 +636,7 @@ impl<'t> Glob<'t> {
.iter()
.filter(|token| token.is_capturing())
.enumerate()
.map(|(index, token)| CapturingToken {
index: index + 1,
span: *token.annotation(),
})
}

/// Returns `true` if the glob has a root.
///
/// As with Unix paths, a glob expression has a root if it begins with a separator `/`.
/// Patterns other than separators may also root an expression, such as `/**` or `</root:1,>`.
pub fn has_root(&self) -> bool {
self.tree.as_ref().as_token().has_root().is_always()
.map(|(index, token)| CapturingToken::new(index + 1, *token.annotation()))
}

/// Returns `true` if the glob has literals that have non-nominal semantics on the target
Expand Down Expand Up @@ -823,8 +699,16 @@ impl<'t> Program<'t> for Glob<'t> {
self.program.captures(path.as_ref()).map(From::from)
}

fn variance(&self) -> Variance {
self.tree.as_ref().as_token().variance::<Text<'t>>().into()
fn depth(&self) -> GlobVariance<Depth> {
self.tree.as_ref().as_token().variance()
}

fn text(&self) -> GlobVariance<Text<'t>> {
self.tree.as_ref().as_token().variance()
}

fn has_root(&self) -> When {
self.tree.as_ref().as_token().has_root()
}

fn is_exhaustive(&self) -> bool {
Expand Down Expand Up @@ -874,8 +758,16 @@ impl<'t> Program<'t> for Any<'t> {
self.program.captures(path.as_ref()).map(From::from)
}

fn variance(&self) -> Variance {
self.tree.as_ref().as_token().variance::<Text<'t>>().into()
fn depth(&self) -> GlobVariance<Depth> {
self.tree.as_ref().as_token().variance()
}

fn text(&self) -> GlobVariance<Text<'t>> {
self.tree.as_ref().as_token().variance()
}

fn has_root(&self) -> When {
self.tree.as_ref().as_token().has_root()
}

fn is_exhaustive(&self) -> bool {
Expand Down Expand Up @@ -2185,7 +2077,7 @@ mod tests {
#[cfg_attr(any(unix, windows), case("[/]root", false))]
fn query_glob_has_root_eq(#[case] expression: &str, #[case] expected: bool) {
let glob = harness::assert_new_glob_is_ok(expression);
let has_root = glob.has_root();
let has_root = glob.has_root().is_always();
assert!(
has_root == expected,
"`Glob::has_root` is `{}`, but expected `{}`: in `Glob`: `{}`",
Expand Down Expand Up @@ -2272,9 +2164,9 @@ mod tests {
#[cfg_attr(unix, case("/a/(?i)file.ext", false))]
#[cfg_attr(windows, case("{a,A}", true))]
#[cfg_attr(windows, case("/a/(?-i)file.ext", false))]
fn query_glob_variance_is_invariant_eq(#[case] expression: &str, #[case] expected: bool) {
fn query_glob_text_is_invariant_eq(#[case] expression: &str, #[case] expected: bool) {
let glob = harness::assert_new_glob_is_ok(expression);
let is_invariant = glob.variance().is_invariant();
let is_invariant = glob.text().is_invariant();
assert!(
is_invariant == expected,
"`Variance::is_invariant` is `{}`, but expected `{}`: in `Glob`: `{}`",
Expand Down

0 comments on commit a2f8764

Please sign in to comment.