forked from mirrors/jj
fileset: implement name resolution stage, add all()/none() functions
#3239
This commit is contained in:
parent
9c28fe954c
commit
653173abad
3 changed files with 222 additions and 8 deletions
|
@ -1,13 +1,9 @@
|
||||||
# Filesets
|
# Filesets
|
||||||
|
|
||||||
<!--
|
|
||||||
TODO: implement fileset parser and add logical operators
|
|
||||||
|
|
||||||
Jujutsu supports a functional language for selecting a set of files.
|
Jujutsu supports a functional language for selecting a set of files.
|
||||||
Expressions in this language are called "filesets" (the idea comes from
|
Expressions in this language are called "filesets" (the idea comes from
|
||||||
[Mercurial](https://repo.mercurial-scm.org/hg/help/filesets)). The language
|
[Mercurial](https://repo.mercurial-scm.org/hg/help/filesets)). The language
|
||||||
consists of symbols, operators, and functions.
|
consists of file patterns, operators, and functions.
|
||||||
-->
|
|
||||||
|
|
||||||
## File patterns
|
## File patterns
|
||||||
|
|
||||||
|
@ -19,3 +15,23 @@ The following patterns are supported:
|
||||||
* `root:"path"`: Matches workspace-relative path prefix (file or files under
|
* `root:"path"`: Matches workspace-relative path prefix (file or files under
|
||||||
directory recursively.)
|
directory recursively.)
|
||||||
* `root-file:"path"`: Matches workspace-relative file (or exact) path.
|
* `root-file:"path"`: Matches workspace-relative file (or exact) path.
|
||||||
|
|
||||||
|
## Operators
|
||||||
|
|
||||||
|
The following operators are supported. `x` and `y` below can be any fileset
|
||||||
|
expressions.
|
||||||
|
|
||||||
|
* `x & y`: Matches both `x` and `y`.
|
||||||
|
* `x | y`: Matches either `x` or `y` (or both).
|
||||||
|
* `x ~ y`: Matches `x` but not `y`.
|
||||||
|
* `~x`: Matches everything but `x`.
|
||||||
|
|
||||||
|
You can use parentheses to control evaluation order, such as `(x & y) | z` or
|
||||||
|
`x & (y | z)`.
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
You can also specify patterns by using functions.
|
||||||
|
|
||||||
|
* `all()`: Matches everything.
|
||||||
|
* `none()`: Matches nothing.
|
||||||
|
|
|
@ -14,11 +14,17 @@
|
||||||
|
|
||||||
//! Functional language for selecting a set of paths.
|
//! Functional language for selecting a set of paths.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::slice;
|
use std::slice;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::dsl_util::collect_similar;
|
||||||
|
use crate::fileset_parser::{
|
||||||
|
self, BinaryOp, ExpressionKind, ExpressionNode, FunctionCallNode, UnaryOp,
|
||||||
|
};
|
||||||
pub use crate::fileset_parser::{FilesetParseError, FilesetParseErrorKind, FilesetParseResult};
|
pub use crate::fileset_parser::{FilesetParseError, FilesetParseErrorKind, FilesetParseResult};
|
||||||
use crate::matchers::{
|
use crate::matchers::{
|
||||||
DifferenceMatcher, EverythingMatcher, FilesMatcher, IntersectionMatcher, Matcher,
|
DifferenceMatcher, EverythingMatcher, FilesMatcher, IntersectionMatcher, Matcher,
|
||||||
|
@ -171,6 +177,18 @@ impl FilesetExpression {
|
||||||
FilesetExpression::Pattern(FilePattern::PrefixPath(path))
|
FilesetExpression::Pattern(FilePattern::PrefixPath(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expression that matches either `self` or `other` (or both).
|
||||||
|
pub fn union(self, other: Self) -> Self {
|
||||||
|
match self {
|
||||||
|
// Micro optimization for "x | y | z"
|
||||||
|
FilesetExpression::UnionAll(mut expressions) => {
|
||||||
|
expressions.push(other);
|
||||||
|
FilesetExpression::UnionAll(expressions)
|
||||||
|
}
|
||||||
|
expr => FilesetExpression::UnionAll(vec![expr, other]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Expression that matches any of the given `expressions`.
|
/// Expression that matches any of the given `expressions`.
|
||||||
pub fn union_all(expressions: Vec<FilesetExpression>) -> Self {
|
pub fn union_all(expressions: Vec<FilesetExpression>) -> Self {
|
||||||
match expressions.len() {
|
match expressions.len() {
|
||||||
|
@ -283,6 +301,87 @@ impl FilesetParseContext<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilesetFunction =
|
||||||
|
fn(&FilesetParseContext, &FunctionCallNode) -> FilesetParseResult<FilesetExpression>;
|
||||||
|
|
||||||
|
static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, FilesetFunction>> = Lazy::new(|| {
|
||||||
|
// Not using maplit::hashmap!{} or custom declarative macro here because
|
||||||
|
// code completion inside macro is quite restricted.
|
||||||
|
let mut map: HashMap<&'static str, FilesetFunction> = HashMap::new();
|
||||||
|
map.insert("none", |_ctx, function| {
|
||||||
|
fileset_parser::expect_no_arguments(function)?;
|
||||||
|
Ok(FilesetExpression::none())
|
||||||
|
});
|
||||||
|
map.insert("all", |_ctx, function| {
|
||||||
|
fileset_parser::expect_no_arguments(function)?;
|
||||||
|
Ok(FilesetExpression::all())
|
||||||
|
});
|
||||||
|
map
|
||||||
|
});
|
||||||
|
|
||||||
|
fn resolve_function(
|
||||||
|
ctx: &FilesetParseContext,
|
||||||
|
function: &FunctionCallNode,
|
||||||
|
) -> FilesetParseResult<FilesetExpression> {
|
||||||
|
if let Some(func) = BUILTIN_FUNCTION_MAP.get(function.name) {
|
||||||
|
func(ctx, function)
|
||||||
|
} else {
|
||||||
|
Err(FilesetParseError::new(
|
||||||
|
FilesetParseErrorKind::NoSuchFunction {
|
||||||
|
name: function.name.to_owned(),
|
||||||
|
candidates: collect_similar(function.name, BUILTIN_FUNCTION_MAP.keys()),
|
||||||
|
},
|
||||||
|
function.name_span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_expression(
|
||||||
|
ctx: &FilesetParseContext,
|
||||||
|
node: &ExpressionNode,
|
||||||
|
) -> FilesetParseResult<FilesetExpression> {
|
||||||
|
let wrap_pattern_error =
|
||||||
|
|err| FilesetParseError::expression("Invalid file pattern", node.span).with_source(err);
|
||||||
|
match &node.kind {
|
||||||
|
ExpressionKind::Identifier(name) => {
|
||||||
|
let pattern = FilePattern::cwd_prefix_path(ctx, name).map_err(wrap_pattern_error)?;
|
||||||
|
Ok(FilesetExpression::pattern(pattern))
|
||||||
|
}
|
||||||
|
ExpressionKind::String(name) => {
|
||||||
|
let pattern = FilePattern::cwd_prefix_path(ctx, name).map_err(wrap_pattern_error)?;
|
||||||
|
Ok(FilesetExpression::pattern(pattern))
|
||||||
|
}
|
||||||
|
ExpressionKind::StringPattern { kind, value } => {
|
||||||
|
let pattern =
|
||||||
|
FilePattern::from_str_kind(ctx, value, kind).map_err(wrap_pattern_error)?;
|
||||||
|
Ok(FilesetExpression::pattern(pattern))
|
||||||
|
}
|
||||||
|
ExpressionKind::Unary(op, arg_node) => {
|
||||||
|
let arg = resolve_expression(ctx, arg_node)?;
|
||||||
|
match op {
|
||||||
|
UnaryOp::Negate => Ok(FilesetExpression::all().difference(arg)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExpressionKind::Binary(op, lhs_node, rhs_node) => {
|
||||||
|
let lhs = resolve_expression(ctx, lhs_node)?;
|
||||||
|
let rhs = resolve_expression(ctx, rhs_node)?;
|
||||||
|
match op {
|
||||||
|
BinaryOp::Union => Ok(lhs.union(rhs)),
|
||||||
|
BinaryOp::Intersection => Ok(lhs.intersection(rhs)),
|
||||||
|
BinaryOp::Difference => Ok(lhs.difference(rhs)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExpressionKind::FunctionCall(function) => resolve_function(ctx, function),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses text into `FilesetExpression`.
|
||||||
|
pub fn parse(text: &str, ctx: &FilesetParseContext) -> FilesetParseResult<FilesetExpression> {
|
||||||
|
let node = fileset_parser::parse_program(text)?;
|
||||||
|
// TODO: add basic tree substitution pass to eliminate redundant expressions
|
||||||
|
resolve_expression(ctx, &node)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -297,7 +396,7 @@ mod tests {
|
||||||
cwd: Path::new("/ws/cur"),
|
cwd: Path::new("/ws/cur"),
|
||||||
workspace_root: Path::new("/ws"),
|
workspace_root: Path::new("/ws"),
|
||||||
};
|
};
|
||||||
// TODO: implement fileset expression parser and test it instead
|
// TODO: adjust identifier rule and test the expression parser instead
|
||||||
let parse = |input| FilePattern::parse(&ctx, input).map(FilesetExpression::pattern);
|
let parse = |input| FilePattern::parse(&ctx, input).map(FilesetExpression::pattern);
|
||||||
|
|
||||||
// cwd-relative patterns
|
// cwd-relative patterns
|
||||||
|
@ -343,6 +442,96 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_function() {
|
||||||
|
let ctx = FilesetParseContext {
|
||||||
|
cwd: Path::new("/ws/cur"),
|
||||||
|
workspace_root: Path::new("/ws"),
|
||||||
|
};
|
||||||
|
let parse = |text| parse(text, &ctx);
|
||||||
|
|
||||||
|
assert_eq!(parse("all()").unwrap(), FilesetExpression::all());
|
||||||
|
assert_eq!(parse("none()").unwrap(), FilesetExpression::none());
|
||||||
|
insta::assert_debug_snapshot!(parse("all(x)").unwrap_err().kind(), @r###"
|
||||||
|
InvalidArguments {
|
||||||
|
name: "all",
|
||||||
|
message: "Expected 0 arguments",
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
insta::assert_debug_snapshot!(parse("ale()").unwrap_err().kind(), @r###"
|
||||||
|
NoSuchFunction {
|
||||||
|
name: "ale",
|
||||||
|
candidates: [
|
||||||
|
"all",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_compound_expression() {
|
||||||
|
let ctx = FilesetParseContext {
|
||||||
|
cwd: Path::new("/ws/cur"),
|
||||||
|
workspace_root: Path::new("/ws"),
|
||||||
|
};
|
||||||
|
let parse = |text| parse(text, &ctx);
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(parse("~x").unwrap(), @r###"
|
||||||
|
Difference(
|
||||||
|
All,
|
||||||
|
Pattern(
|
||||||
|
PrefixPath(
|
||||||
|
"cur/x",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
"###);
|
||||||
|
insta::assert_debug_snapshot!(parse("x|y|root:z").unwrap(), @r###"
|
||||||
|
UnionAll(
|
||||||
|
[
|
||||||
|
Pattern(
|
||||||
|
PrefixPath(
|
||||||
|
"cur/x",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Pattern(
|
||||||
|
PrefixPath(
|
||||||
|
"cur/y",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Pattern(
|
||||||
|
PrefixPath(
|
||||||
|
"z",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
"###);
|
||||||
|
insta::assert_debug_snapshot!(parse("x|y&z").unwrap(), @r###"
|
||||||
|
UnionAll(
|
||||||
|
[
|
||||||
|
Pattern(
|
||||||
|
PrefixPath(
|
||||||
|
"cur/x",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Intersection(
|
||||||
|
Pattern(
|
||||||
|
PrefixPath(
|
||||||
|
"cur/y",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Pattern(
|
||||||
|
PrefixPath(
|
||||||
|
"cur/z",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_matcher_simple() {
|
fn test_build_matcher_simple() {
|
||||||
insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
|
insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
|
||||||
|
|
|
@ -14,8 +14,6 @@
|
||||||
|
|
||||||
//! Parser for the fileset language.
|
//! Parser for the fileset language.
|
||||||
|
|
||||||
#![allow(unused)] // TODO
|
|
||||||
|
|
||||||
use std::error;
|
use std::error;
|
||||||
|
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
|
@ -303,6 +301,17 @@ pub fn parse_program(text: &str) -> FilesetParseResult<ExpressionNode> {
|
||||||
parse_expression_node(first)
|
parse_expression_node(first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn expect_no_arguments(function: &FunctionCallNode) -> FilesetParseResult<()> {
|
||||||
|
if function.args.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(FilesetParseError::invalid_arguments(
|
||||||
|
function,
|
||||||
|
"Expected 0 arguments",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use assert_matches::assert_matches;
|
use assert_matches::assert_matches;
|
||||||
|
|
Loading…
Reference in a new issue