fileset: add recursive iterator over explicit paths

The primary use case is to warn unmatched paths. I originally thought paths in
negated expressions shouldn't be checked, but doing that seems rather
inconsistent than useful. For example, "~x" in "jj split '~x'" should match at
least one file to split to non-empty revisions.
This commit is contained in:
Yuya Nishihara 2024-04-09 16:27:04 +09:00
parent 580a90b694
commit 33beb8d456

View file

@ -16,7 +16,7 @@
use std::collections::HashMap;
use std::path::Path;
use std::slice;
use std::{iter, slice};
use once_cell::sync::Lazy;
use thiserror::Error;
@ -30,7 +30,7 @@ use crate::matchers::{
DifferenceMatcher, EverythingMatcher, FilesMatcher, IntersectionMatcher, Matcher,
NothingMatcher, PrefixMatcher, UnionMatcher,
};
use crate::repo_path::{FsPathParseError, RelativePathParseError, RepoPathBuf};
use crate::repo_path::{FsPathParseError, RelativePathParseError, RepoPath, RepoPathBuf};
/// Error occurred during file pattern parsing.
#[derive(Debug, Error)]
@ -129,6 +129,15 @@ impl FilePattern {
let path = RepoPathBuf::from_relative_path(input)?;
Ok(FilePattern::PrefixPath(path))
}
/// Returns path if this pattern represents a literal path in a workspace.
/// Returns `None` if this is a glob pattern for example.
pub fn as_path(&self) -> Option<&RepoPath> {
match self {
FilePattern::FilePath(path) => Some(path),
FilePattern::PrefixPath(path) => Some(path),
}
}
}
/// AST-level representation of the fileset expression.
@ -217,6 +226,38 @@ impl FilesetExpression {
}
}
fn dfs_pre(&self) -> impl Iterator<Item = &Self> {
let mut stack: Vec<&Self> = vec![self];
iter::from_fn(move || {
let expr = stack.pop()?;
match expr {
FilesetExpression::None
| FilesetExpression::All
| FilesetExpression::Pattern(_) => {}
FilesetExpression::UnionAll(exprs) => stack.extend(exprs.iter().rev()),
FilesetExpression::Intersection(expr1, expr2)
| FilesetExpression::Difference(expr1, expr2) => {
stack.push(expr2);
stack.push(expr1);
}
}
Some(expr)
})
}
/// Iterates literal paths recursively from this expression.
///
/// For example, `"a", "b", "c"` will be yielded in that order for
/// expression `"a" | all() & "b" | ~"c"`.
pub fn explicit_paths(&self) -> impl Iterator<Item = &RepoPath> {
// pre/post-ordering doesn't matter so long as children are visited from
// left to right.
self.dfs_pre().flat_map(|expr| match expr {
FilesetExpression::Pattern(pattern) => pattern.as_path(),
_ => None,
})
}
/// Transforms the expression tree to `Matcher` object.
pub fn to_matcher(&self) -> Box<dyn Matcher> {
build_union_matcher(self.as_union_all())
@ -531,6 +572,39 @@ mod tests {
"###);
}
#[test]
fn test_explicit_paths() {
let collect = |expr: &FilesetExpression| -> Vec<RepoPathBuf> {
expr.explicit_paths().map(|path| path.to_owned()).collect()
};
let file_expr = |path: &str| FilesetExpression::file_path(repo_path_buf(path));
assert!(collect(&FilesetExpression::none()).is_empty());
assert_eq!(collect(&file_expr("a")), ["a"].map(repo_path_buf));
assert_eq!(
collect(&FilesetExpression::union_all(vec![
file_expr("a"),
file_expr("b"),
file_expr("c"),
])),
["a", "b", "c"].map(repo_path_buf)
);
assert_eq!(
collect(&FilesetExpression::intersection(
FilesetExpression::union_all(vec![
file_expr("a"),
FilesetExpression::none(),
file_expr("b"),
file_expr("c"),
]),
FilesetExpression::difference(
file_expr("d"),
FilesetExpression::union_all(vec![file_expr("e"), file_expr("f")])
)
)),
["a", "b", "c", "d", "e", "f"].map(repo_path_buf)
);
}
#[test]
fn test_build_matcher_simple() {
insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");