Skip to content

Commit

Permalink
fileset: implement name resolution stage, add all()/none() functions
Browse files Browse the repository at this point in the history
  • Loading branch information
yuja committed Apr 9, 2024
1 parent 9c28fe9 commit 653173a
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 8 deletions.
26 changes: 21 additions & 5 deletions docs/filesets.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# Filesets

<!--
TODO: implement fileset parser and add logical operators
Jujutsu supports a functional language for selecting a set of files.
Expressions in this language are called "filesets" (the idea comes from
[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

Expand All @@ -19,3 +15,23 @@ The following patterns are supported:
* `root:"path"`: Matches workspace-relative path prefix (file or files under
directory recursively.)
* `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.
191 changes: 190 additions & 1 deletion lib/src/fileset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@

//! Functional language for selecting a set of paths.
use std::collections::HashMap;
use std::path::Path;
use std::slice;

use once_cell::sync::Lazy;
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};
use crate::matchers::{
DifferenceMatcher, EverythingMatcher, FilesMatcher, IntersectionMatcher, Matcher,
Expand Down Expand Up @@ -171,6 +177,18 @@ impl FilesetExpression {
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`.
pub fn union_all(expressions: Vec<FilesetExpression>) -> Self {
match expressions.len() {
Expand Down Expand Up @@ -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)]
mod tests {
use super::*;
Expand All @@ -297,7 +396,7 @@ mod tests {
cwd: Path::new("/ws/cur"),
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);

// cwd-relative patterns
Expand Down Expand Up @@ -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]
fn test_build_matcher_simple() {
insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
Expand Down
13 changes: 11 additions & 2 deletions lib/src/fileset_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

//! Parser for the fileset language.
#![allow(unused)] // TODO

use std::error;

use itertools::Itertools as _;
Expand Down Expand Up @@ -303,6 +301,17 @@ pub fn parse_program(text: &str) -> FilesetParseResult<ExpressionNode> {
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)]
mod tests {
use assert_matches::assert_matches;
Expand Down

0 comments on commit 653173a

Please sign in to comment.