// Copyright 2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. //! Parser for the fileset language. use std::error;use std::sync::LazyLock; use itertools::Itertools as _;use pest::iterators::Pair;use pest::pratt_parser::Assoc;use pest::pratt_parser::Op;use pest::pratt_parser::PrattParser;use pest::Parser as _;use pest_derive::Parser;use thiserror::Error; use crate::dsl_util;use crate::dsl_util::Diagnostics;use crate::dsl_util::InvalidArguments;use crate::dsl_util::StringLiteralParser; #[derive(Parser)]#[grammar = "fileset.pest"]struct FilesetParser; const STRING_LITERAL_PARSER: StringLiteralParser<Rule> = StringLiteralParser { content_rule: Rule::string_content, escape_rule: Rule::string_escape,}; impl Rule { fn to_symbol(self) -> Option<&'static str> { match self { Rule::EOI => None, Rule::whitespace => None, Rule::identifier => None, Rule::strict_identifier_part => None, Rule::strict_identifier => None, Rule::bare_string => None, Rule::string_escape => None, Rule::string_content_char => None, Rule::string_content => None, Rule::string_literal => None, Rule::raw_string_content => None, Rule::raw_string_literal => None, Rule::pattern_kind_op => Some(":"), Rule::negate_op => Some("~"), Rule::union_op => Some("|"), Rule::intersection_op => Some("&"), Rule::difference_op => Some("~"), Rule::prefix_ops => None, Rule::infix_ops => None, Rule::function => None, Rule::function_name => None, Rule::function_arguments => None, Rule::string_pattern => None, Rule::bare_string_pattern => None, Rule::primary => None, Rule::expression => None, Rule::program => None, Rule::program_or_bare_string => None, } }} /// Manages diagnostic messages emitted during fileset parsing and name/// resolution.pub type FilesetDiagnostics = Diagnostics<FilesetParseError>; /// Result of fileset parsing and name resolution.pub type FilesetParseResult<T> = Result<T, FilesetParseError>; /// Error occurred during fileset parsing and name resolution.#[derive(Debug, Error)]#[error("{pest_error}")]pub struct FilesetParseError { kind: FilesetParseErrorKind, pest_error: Box<pest::error::Error<Rule>>, source: Option<Box<dyn error::Error + Send + Sync>>,} /// Categories of fileset parsing and name resolution error.#[expect(missing_docs)]#[derive(Clone, Debug, Eq, Error, PartialEq)]pub enum FilesetParseErrorKind { #[error("Syntax error")] SyntaxError, #[error("Function `{name}` doesn't exist")] NoSuchFunction { name: String, candidates: Vec<String>, }, #[error("Function `{name}`: {message}")] InvalidArguments { name: String, message: String }, #[error("{0}")] Expression(String),} impl FilesetParseError { pub(super) fn new(kind: FilesetParseErrorKind, span: pest::Span<'_>) -> Self { let message = kind.to_string(); let pest_error = Box::new(pest::error::Error::new_from_span( pest::error::ErrorVariant::CustomError { message }, span, )); FilesetParseError { kind, pest_error, source: None, } } pub(super) fn with_source( mut self, source: impl Into<Box<dyn error::Error + Send + Sync>>, ) -> Self { self.source = Some(source.into()); self } /// Some other expression error. pub(super) fn expression(message: impl Into<String>, span: pest::Span<'_>) -> Self { FilesetParseError::new(FilesetParseErrorKind::Expression(message.into()), span) } /// Category of the underlying error. pub fn kind(&self) -> &FilesetParseErrorKind { &self.kind }} impl From<pest::error::Error<Rule>> for FilesetParseError { fn from(err: pest::error::Error<Rule>) -> Self { FilesetParseError { kind: FilesetParseErrorKind::SyntaxError, pest_error: Box::new(rename_rules_in_pest_error(err)), source: None, } }} impl From<InvalidArguments<'_>> for FilesetParseError { fn from(err: InvalidArguments<'_>) -> Self { let kind = FilesetParseErrorKind::InvalidArguments { name: err.name.to_owned(), message: err.message, }; Self::new(kind, err.span) }} fn rename_rules_in_pest_error(err: pest::error::Error<Rule>) -> pest::error::Error<Rule> { err.renamed_rules(|rule| { rule.to_symbol() .map(|sym| format!("`{sym}`")) .unwrap_or_else(|| format!("<{rule:?}>")) })} #[derive(Clone, Debug, Eq, PartialEq)]pub enum ExpressionKind<'i> { Identifier(&'i str), String(String), StringPattern { kind: &'i str, value: String, }, Unary(UnaryOp, Box<ExpressionNode<'i>>), Binary(BinaryOp, Box<ExpressionNode<'i>>, Box<ExpressionNode<'i>>), /// `x | y | ..` UnionAll(Vec<ExpressionNode<'i>>), FunctionCall(Box<FunctionCallNode<'i>>),} #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]pub enum UnaryOp { /// `~` Negate,} #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]pub enum BinaryOp { /// `&` Intersection, /// `~` Difference,} pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>;pub type FunctionCallNode<'i> = dsl_util::FunctionCallNode<'i, ExpressionKind<'i>>; fn union_nodes<'i>(lhs: ExpressionNode<'i>, rhs: ExpressionNode<'i>) -> ExpressionNode<'i> { let span = lhs.span.start_pos().span(&rhs.span.end_pos()); let expr = match lhs.kind { // Flatten "x | y | z" to save recursion stack. Machine-generated query // might have long chain of unions. ExpressionKind::UnionAll(mut nodes) => { nodes.push(rhs); ExpressionKind::UnionAll(nodes) } _ => ExpressionKind::UnionAll(vec![lhs, rhs]), }; ExpressionNode::new(expr, span)} fn parse_function_call_node(pair: Pair<Rule>) -> FilesetParseResult<FunctionCallNode> { assert_eq!(pair.as_rule(), Rule::function); let [name_pair, args_pair] = pair.into_inner().collect_array().unwrap(); assert_eq!(name_pair.as_rule(), Rule::function_name); assert_eq!(args_pair.as_rule(), Rule::function_arguments); let name_span = name_pair.as_span(); let args_span = args_pair.as_span(); let name = name_pair.as_str(); let args = args_pair .into_inner() .map(parse_expression_node) .try_collect()?; Ok(FunctionCallNode { name, name_span, args, keyword_args: vec![], // unsupported args_span, })} fn parse_as_string_literal(pair: Pair<Rule>) -> String { match pair.as_rule() { Rule::identifier => pair.as_str().to_owned(), Rule::string_literal => STRING_LITERAL_PARSER.parse(pair.into_inner()), Rule::raw_string_literal => { let [content] = pair.into_inner().collect_array().unwrap(); assert_eq!(content.as_rule(), Rule::raw_string_content); content.as_str().to_owned() } r => panic!("unexpected string literal rule: {r:?}"), }} fn parse_primary_node(pair: Pair<Rule>) -> FilesetParseResult<ExpressionNode> { assert_eq!(pair.as_rule(), Rule::primary); let first = pair.into_inner().next().unwrap(); let span = first.as_span(); let expr = match first.as_rule() { Rule::expression => return parse_expression_node(first), Rule::function => { let function = Box::new(parse_function_call_node(first)?); ExpressionKind::FunctionCall(function) } Rule::string_pattern => { let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); assert_eq!(lhs.as_rule(), Rule::strict_identifier); assert_eq!(op.as_rule(), Rule::pattern_kind_op); let kind = lhs.as_str(); let value = parse_as_string_literal(rhs); ExpressionKind::StringPattern { kind, value } } Rule::identifier => ExpressionKind::Identifier(first.as_str()), Rule::string_literal | Rule::raw_string_literal => { ExpressionKind::String(parse_as_string_literal(first)) } r => panic!("unexpected primary rule: {r:?}"), }; Ok(ExpressionNode::new(expr, span))} fn parse_expression_node(pair: Pair<Rule>) -> FilesetParseResult<ExpressionNode> { assert_eq!(pair.as_rule(), Rule::expression); static PRATT: LazyLock<PrattParser<Rule>> = LazyLock::new(|| { PrattParser::new() .op(Op::infix(Rule::union_op, Assoc::Left)) .op(Op::infix(Rule::intersection_op, Assoc::Left) | Op::infix(Rule::difference_op, Assoc::Left)) .op(Op::prefix(Rule::negate_op)) }); PRATT .map_primary(parse_primary_node) .map_prefix(|op, rhs| { let op_kind = match op.as_rule() { Rule::negate_op => UnaryOp::Negate, r => panic!("unexpected prefix operator rule {r:?}"), }; let rhs = Box::new(rhs?); let span = op.as_span().start_pos().span(&rhs.span.end_pos()); let expr = ExpressionKind::Unary(op_kind, rhs); Ok(ExpressionNode::new(expr, span)) }) .map_infix(|lhs, op, rhs| { let op_kind = match op.as_rule() { Rule::union_op => return Ok(union_nodes(lhs?, rhs?)), Rule::intersection_op => BinaryOp::Intersection, Rule::difference_op => BinaryOp::Difference, r => panic!("unexpected infix operator rule {r:?}"), }; let lhs = Box::new(lhs?); let rhs = Box::new(rhs?); let span = lhs.span.start_pos().span(&rhs.span.end_pos()); let expr = ExpressionKind::Binary(op_kind, lhs, rhs); Ok(ExpressionNode::new(expr, span)) }) .parse(pair.into_inner())} /// Parses text into expression tree. No name resolution is made at this stage.pub fn parse_program(text: &str) -> FilesetParseResult<ExpressionNode<'_>> { let mut pairs = FilesetParser::parse(Rule::program, text)?; let first = pairs.next().unwrap(); parse_expression_node(first)} /// Parses text into expression tree with bare string fallback. No name/// resolution is made at this stage.////// If the text can't be parsed as a fileset expression, and if it doesn't/// contain any operator-like characters, it will be parsed as a file path.pub fn parse_program_or_bare_string(text: &str) -> FilesetParseResult<ExpressionNode<'_>> { let mut pairs = FilesetParser::parse(Rule::program_or_bare_string, text)?; let first = pairs.next().unwrap(); let span = first.as_span(); let expr = match first.as_rule() { Rule::expression => return parse_expression_node(first), Rule::bare_string_pattern => { let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); assert_eq!(lhs.as_rule(), Rule::strict_identifier); assert_eq!(op.as_rule(), Rule::pattern_kind_op); assert_eq!(rhs.as_rule(), Rule::bare_string); let kind = lhs.as_str(); let value = rhs.as_str().to_owned(); ExpressionKind::StringPattern { kind, value } } Rule::bare_string => ExpressionKind::String(first.as_str().to_owned()), r => panic!("unexpected program or bare string rule: {r:?}"), }; Ok(ExpressionNode::new(expr, span))} #[cfg(test)]mod tests { use assert_matches::assert_matches; use super::*; use crate::dsl_util::KeywordArgument; fn parse_into_kind(text: &str) -> Result<ExpressionKind<'_>, FilesetParseErrorKind> { parse_program(text) .map(|node| node.kind) .map_err(|err| err.kind) } fn parse_maybe_bare_into_kind(text: &str) -> Result<ExpressionKind<'_>, FilesetParseErrorKind> { parse_program_or_bare_string(text) .map(|node| node.kind) .map_err(|err| err.kind) } fn parse_normalized(text: &str) -> ExpressionNode<'_> { normalize_tree(parse_program(text).unwrap()) } fn parse_maybe_bare_normalized(text: &str) -> ExpressionNode<'_> { normalize_tree(parse_program_or_bare_string(text).unwrap()) } /// Drops auxiliary data from parsed tree so it can be compared with other. fn normalize_tree(node: ExpressionNode) -> ExpressionNode { fn empty_span() -> pest::Span<'static> { pest::Span::new("", 0, 0).unwrap() } fn normalize_list(nodes: Vec<ExpressionNode>) -> Vec<ExpressionNode> { nodes.into_iter().map(normalize_tree).collect() } fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode { FunctionCallNode { name: function.name, name_span: empty_span(), args: normalize_list(function.args), keyword_args: function .keyword_args .into_iter() .map(|arg| KeywordArgument { name: arg.name, name_span: empty_span(), value: normalize_tree(arg.value), }) .collect(), args_span: empty_span(), } } let normalized_kind = match node.kind { ExpressionKind::Identifier(_) | ExpressionKind::String(_) | ExpressionKind::StringPattern { .. } => node.kind, ExpressionKind::Unary(op, arg) => { let arg = Box::new(normalize_tree(*arg)); ExpressionKind::Unary(op, arg) } ExpressionKind::Binary(op, lhs, rhs) => { let lhs = Box::new(normalize_tree(*lhs)); let rhs = Box::new(normalize_tree(*rhs)); ExpressionKind::Binary(op, lhs, rhs) } ExpressionKind::UnionAll(nodes) => { let nodes = normalize_list(nodes); ExpressionKind::UnionAll(nodes) } ExpressionKind::FunctionCall(function) => { let function = Box::new(normalize_function_call(*function)); ExpressionKind::FunctionCall(function) } }; ExpressionNode { kind: normalized_kind, span: empty_span(), } } #[test] fn test_parse_tree_eq() { assert_eq!( parse_normalized(r#" foo( x ) | ~bar:"baz" "#), parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#) ); assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#)); } #[test] fn test_parse_invalid_function_name() { assert_eq!( parse_into_kind("5foo(x)"), Err(FilesetParseErrorKind::SyntaxError) ); } #[test] fn test_parse_whitespace() { let ascii_whitespaces: String = ('\x00'..='\x7f') .filter(char::is_ascii_whitespace) .collect(); assert_eq!( parse_normalized(&format!("{ascii_whitespaces}f()")), parse_normalized("f()") ); } #[test] fn test_parse_identifier() { assert_eq!( parse_into_kind("dir/foo-bar_0.baz"), Ok(ExpressionKind::Identifier("dir/foo-bar_0.baz")) ); assert_eq!( parse_into_kind("cli-reference@.md.snap"), Ok(ExpressionKind::Identifier("cli-reference@.md.snap")) ); assert_eq!( parse_into_kind("柔術.jj"), Ok(ExpressionKind::Identifier("柔術.jj")) ); assert_eq!( parse_into_kind(r#"Windows\Path"#), Ok(ExpressionKind::Identifier(r#"Windows\Path"#)) ); assert_eq!( parse_into_kind("glob*[chars]?"), Ok(ExpressionKind::Identifier("glob*[chars]?")) ); } #[test] fn test_parse_string_literal() { // "\<char>" escapes assert_eq!( parse_into_kind(r#" "\t\r\n\"\\\0\e" "#), Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())), ); // Invalid "\<char>" escape assert_eq!( parse_into_kind(r#" "\y" "#), Err(FilesetParseErrorKind::SyntaxError), ); // Single-quoted raw string assert_eq!( parse_into_kind(r#" '' "#),Lines 1–500 of 737. Load more
// Copyright 2024 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Parser for the fileset language. use std::error; use std::sync::LazyLock; use itertools::Itertools as _; use pest::iterators::Pair; use pest::pratt_parser::Assoc; use pest::pratt_parser::Op; use pest::pratt_parser::PrattParser; use pest::Parser as _; use pest_derive::Parser; use thiserror::Error; use crate::dsl_util; use crate::dsl_util::Diagnostics; use crate::dsl_util::InvalidArguments; use crate::dsl_util::StringLiteralParser; #[derive(Parser)] #[grammar = "fileset.pest"] struct FilesetParser; const STRING_LITERAL_PARSER: StringLiteralParser<Rule> = StringLiteralParser { content_rule: Rule::string_content, escape_rule: Rule::string_escape, }; impl Rule { fn to_symbol(self) -> Option<&'static str> { match self { Rule::EOI => None, Rule::whitespace => None, Rule::identifier => None, Rule::strict_identifier_part => None, Rule::strict_identifier => None, Rule::bare_string => None, Rule::string_escape => None, Rule::string_content_char => None, Rule::string_content => None, Rule::string_literal => None, Rule::raw_string_content => None, Rule::raw_string_literal => None, Rule::pattern_kind_op => Some(":"), Rule::negate_op => Some("~"), Rule::union_op => Some("|"), Rule::intersection_op => Some("&"), Rule::difference_op => Some("~"), Rule::prefix_ops => None, Rule::infix_ops => None, Rule::function => None, Rule::function_name => None, Rule::function_arguments => None, Rule::string_pattern => None, Rule::bare_string_pattern => None, Rule::primary => None, Rule::expression => None, Rule::program => None, Rule::program_or_bare_string => None, } } } /// Manages diagnostic messages emitted during fileset parsing and name /// resolution. pub type FilesetDiagnostics = Diagnostics<FilesetParseError>; /// Result of fileset parsing and name resolution. pub type FilesetParseResult<T> = Result<T, FilesetParseError>; /// Error occurred during fileset parsing and name resolution. #[derive(Debug, Error)] #[error("{pest_error}")] pub struct FilesetParseError { kind: FilesetParseErrorKind, pest_error: Box<pest::error::Error<Rule>>, source: Option<Box<dyn error::Error + Send + Sync>>, } /// Categories of fileset parsing and name resolution error. #[expect(missing_docs)] #[derive(Clone, Debug, Eq, Error, PartialEq)] pub enum FilesetParseErrorKind { #[error("Syntax error")] SyntaxError, #[error("Function `{name}` doesn't exist")] NoSuchFunction { name: String, candidates: Vec<String>, }, #[error("Function `{name}`: {message}")] InvalidArguments { name: String, message: String }, #[error("{0}")] Expression(String), } impl FilesetParseError { pub(super) fn new(kind: FilesetParseErrorKind, span: pest::Span<'_>) -> Self { let message = kind.to_string(); let pest_error = Box::new(pest::error::Error::new_from_span( pest::error::ErrorVariant::CustomError { message }, span, )); FilesetParseError { kind, pest_error, source: None, } } pub(super) fn with_source( mut self, source: impl Into<Box<dyn error::Error + Send + Sync>>, ) -> Self { self.source = Some(source.into()); self } /// Some other expression error. pub(super) fn expression(message: impl Into<String>, span: pest::Span<'_>) -> Self { FilesetParseError::new(FilesetParseErrorKind::Expression(message.into()), span) } /// Category of the underlying error. pub fn kind(&self) -> &FilesetParseErrorKind { &self.kind } } impl From<pest::error::Error<Rule>> for FilesetParseError { fn from(err: pest::error::Error<Rule>) -> Self { FilesetParseError { kind: FilesetParseErrorKind::SyntaxError, pest_error: Box::new(rename_rules_in_pest_error(err)), source: None, } } } impl From<InvalidArguments<'_>> for FilesetParseError { fn from(err: InvalidArguments<'_>) -> Self { let kind = FilesetParseErrorKind::InvalidArguments { name: err.name.to_owned(), message: err.message, }; Self::new(kind, err.span) } } fn rename_rules_in_pest_error(err: pest::error::Error<Rule>) -> pest::error::Error<Rule> { err.renamed_rules(|rule| { rule.to_symbol() .map(|sym| format!("`{sym}`")) .unwrap_or_else(|| format!("<{rule:?}>")) }) } #[derive(Clone, Debug, Eq, PartialEq)] pub enum ExpressionKind<'i> { Identifier(&'i str), String(String), StringPattern { kind: &'i str, value: String, }, Unary(UnaryOp, Box<ExpressionNode<'i>>), Binary(BinaryOp, Box<ExpressionNode<'i>>, Box<ExpressionNode<'i>>), /// `x | y | ..` UnionAll(Vec<ExpressionNode<'i>>), FunctionCall(Box<FunctionCallNode<'i>>), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum UnaryOp { /// `~` Negate, } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum BinaryOp { /// `&` Intersection, /// `~` Difference, } pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>; pub type FunctionCallNode<'i> = dsl_util::FunctionCallNode<'i, ExpressionKind<'i>>; fn union_nodes<'i>(lhs: ExpressionNode<'i>, rhs: ExpressionNode<'i>) -> ExpressionNode<'i> { let span = lhs.span.start_pos().span(&rhs.span.end_pos()); let expr = match lhs.kind { // Flatten "x | y | z" to save recursion stack. Machine-generated query // might have long chain of unions. ExpressionKind::UnionAll(mut nodes) => { nodes.push(rhs); ExpressionKind::UnionAll(nodes) } _ => ExpressionKind::UnionAll(vec![lhs, rhs]), }; ExpressionNode::new(expr, span) } fn parse_function_call_node(pair: Pair<Rule>) -> FilesetParseResult<FunctionCallNode> { assert_eq!(pair.as_rule(), Rule::function); let [name_pair, args_pair] = pair.into_inner().collect_array().unwrap(); assert_eq!(name_pair.as_rule(), Rule::function_name); assert_eq!(args_pair.as_rule(), Rule::function_arguments); let name_span = name_pair.as_span(); let args_span = args_pair.as_span(); let name = name_pair.as_str(); let args = args_pair .into_inner() .map(parse_expression_node) .try_collect()?; Ok(FunctionCallNode { name, name_span, args, keyword_args: vec![], // unsupported args_span, }) } fn parse_as_string_literal(pair: Pair<Rule>) -> String { match pair.as_rule() { Rule::identifier => pair.as_str().to_owned(), Rule::string_literal => STRING_LITERAL_PARSER.parse(pair.into_inner()), Rule::raw_string_literal => { let [content] = pair.into_inner().collect_array().unwrap(); assert_eq!(content.as_rule(), Rule::raw_string_content); content.as_str().to_owned() } r => panic!("unexpected string literal rule: {r:?}"), } } fn parse_primary_node(pair: Pair<Rule>) -> FilesetParseResult<ExpressionNode> { assert_eq!(pair.as_rule(), Rule::primary); let first = pair.into_inner().next().unwrap(); let span = first.as_span(); let expr = match first.as_rule() { Rule::expression => return parse_expression_node(first), Rule::function => { let function = Box::new(parse_function_call_node(first)?); ExpressionKind::FunctionCall(function) } Rule::string_pattern => { let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); assert_eq!(lhs.as_rule(), Rule::strict_identifier); assert_eq!(op.as_rule(), Rule::pattern_kind_op); let kind = lhs.as_str(); let value = parse_as_string_literal(rhs); ExpressionKind::StringPattern { kind, value } } Rule::identifier => ExpressionKind::Identifier(first.as_str()), Rule::string_literal | Rule::raw_string_literal => { ExpressionKind::String(parse_as_string_literal(first)) } r => panic!("unexpected primary rule: {r:?}"), }; Ok(ExpressionNode::new(expr, span)) } fn parse_expression_node(pair: Pair<Rule>) -> FilesetParseResult<ExpressionNode> { assert_eq!(pair.as_rule(), Rule::expression); static PRATT: LazyLock<PrattParser<Rule>> = LazyLock::new(|| { PrattParser::new() .op(Op::infix(Rule::union_op, Assoc::Left)) .op(Op::infix(Rule::intersection_op, Assoc::Left) | Op::infix(Rule::difference_op, Assoc::Left)) .op(Op::prefix(Rule::negate_op)) }); PRATT .map_primary(parse_primary_node) .map_prefix(|op, rhs| { let op_kind = match op.as_rule() { Rule::negate_op => UnaryOp::Negate, r => panic!("unexpected prefix operator rule {r:?}"), }; let rhs = Box::new(rhs?); let span = op.as_span().start_pos().span(&rhs.span.end_pos()); let expr = ExpressionKind::Unary(op_kind, rhs); Ok(ExpressionNode::new(expr, span)) }) .map_infix(|lhs, op, rhs| { let op_kind = match op.as_rule() { Rule::union_op => return Ok(union_nodes(lhs?, rhs?)), Rule::intersection_op => BinaryOp::Intersection, Rule::difference_op => BinaryOp::Difference, r => panic!("unexpected infix operator rule {r:?}"), }; let lhs = Box::new(lhs?); let rhs = Box::new(rhs?); let span = lhs.span.start_pos().span(&rhs.span.end_pos()); let expr = ExpressionKind::Binary(op_kind, lhs, rhs); Ok(ExpressionNode::new(expr, span)) }) .parse(pair.into_inner()) } /// Parses text into expression tree. No name resolution is made at this stage. pub fn parse_program(text: &str) -> FilesetParseResult<ExpressionNode<'_>> { let mut pairs = FilesetParser::parse(Rule::program, text)?; let first = pairs.next().unwrap(); parse_expression_node(first) } /// Parses text into expression tree with bare string fallback. No name /// resolution is made at this stage. /// /// If the text can't be parsed as a fileset expression, and if it doesn't /// contain any operator-like characters, it will be parsed as a file path. pub fn parse_program_or_bare_string(text: &str) -> FilesetParseResult<ExpressionNode<'_>> { let mut pairs = FilesetParser::parse(Rule::program_or_bare_string, text)?; let first = pairs.next().unwrap(); let span = first.as_span(); let expr = match first.as_rule() { Rule::expression => return parse_expression_node(first), Rule::bare_string_pattern => { let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); assert_eq!(lhs.as_rule(), Rule::strict_identifier); assert_eq!(op.as_rule(), Rule::pattern_kind_op); assert_eq!(rhs.as_rule(), Rule::bare_string); let kind = lhs.as_str(); let value = rhs.as_str().to_owned(); ExpressionKind::StringPattern { kind, value } } Rule::bare_string => ExpressionKind::String(first.as_str().to_owned()), r => panic!("unexpected program or bare string rule: {r:?}"), }; Ok(ExpressionNode::new(expr, span)) } #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::*; use crate::dsl_util::KeywordArgument; fn parse_into_kind(text: &str) -> Result<ExpressionKind<'_>, FilesetParseErrorKind> { parse_program(text) .map(|node| node.kind) .map_err(|err| err.kind) } fn parse_maybe_bare_into_kind(text: &str) -> Result<ExpressionKind<'_>, FilesetParseErrorKind> { parse_program_or_bare_string(text) .map(|node| node.kind) .map_err(|err| err.kind) } fn parse_normalized(text: &str) -> ExpressionNode<'_> { normalize_tree(parse_program(text).unwrap()) } fn parse_maybe_bare_normalized(text: &str) -> ExpressionNode<'_> { normalize_tree(parse_program_or_bare_string(text).unwrap()) } /// Drops auxiliary data from parsed tree so it can be compared with other. fn normalize_tree(node: ExpressionNode) -> ExpressionNode { fn empty_span() -> pest::Span<'static> { pest::Span::new("", 0, 0).unwrap() } fn normalize_list(nodes: Vec<ExpressionNode>) -> Vec<ExpressionNode> { nodes.into_iter().map(normalize_tree).collect() } fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode { FunctionCallNode { name: function.name, name_span: empty_span(), args: normalize_list(function.args), keyword_args: function .keyword_args .into_iter() .map(|arg| KeywordArgument { name: arg.name, name_span: empty_span(), value: normalize_tree(arg.value), }) .collect(), args_span: empty_span(), } } let normalized_kind = match node.kind { ExpressionKind::Identifier(_) | ExpressionKind::String(_) | ExpressionKind::StringPattern { .. } => node.kind, ExpressionKind::Unary(op, arg) => { let arg = Box::new(normalize_tree(*arg)); ExpressionKind::Unary(op, arg) } ExpressionKind::Binary(op, lhs, rhs) => { let lhs = Box::new(normalize_tree(*lhs)); let rhs = Box::new(normalize_tree(*rhs)); ExpressionKind::Binary(op, lhs, rhs) } ExpressionKind::UnionAll(nodes) => { let nodes = normalize_list(nodes); ExpressionKind::UnionAll(nodes) } ExpressionKind::FunctionCall(function) => { let function = Box::new(normalize_function_call(*function)); ExpressionKind::FunctionCall(function) } }; ExpressionNode { kind: normalized_kind, span: empty_span(), } } #[test] fn test_parse_tree_eq() { assert_eq!( parse_normalized(r#" foo( x ) | ~bar:"baz" "#), parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#) ); assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#)); } #[test] fn test_parse_invalid_function_name() { assert_eq!( parse_into_kind("5foo(x)"), Err(FilesetParseErrorKind::SyntaxError) ); } #[test] fn test_parse_whitespace() { let ascii_whitespaces: String = ('\x00'..='\x7f') .filter(char::is_ascii_whitespace) .collect(); assert_eq!( parse_normalized(&format!("{ascii_whitespaces}f()")), parse_normalized("f()") ); } #[test] fn test_parse_identifier() { assert_eq!( parse_into_kind("dir/foo-bar_0.baz"), Ok(ExpressionKind::Identifier("dir/foo-bar_0.baz")) ); assert_eq!( parse_into_kind("cli-reference@.md.snap"), Ok(ExpressionKind::Identifier("cli-reference@.md.snap")) ); assert_eq!( parse_into_kind("柔術.jj"), Ok(ExpressionKind::Identifier("柔術.jj")) ); assert_eq!( parse_into_kind(r#"Windows\Path"#), Ok(ExpressionKind::Identifier(r#"Windows\Path"#)) ); assert_eq!( parse_into_kind("glob*[chars]?"), Ok(ExpressionKind::Identifier("glob*[chars]?")) ); } #[test] fn test_parse_string_literal() { // "\<char>" escapes assert_eq!( parse_into_kind(r#" "\t\r\n\"\\\0\e" "#), Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())), ); // Invalid "\<char>" escape assert_eq!( parse_into_kind(r#" "\y" "#), Err(FilesetParseErrorKind::SyntaxError), ); // Single-quoted raw string assert_eq!( parse_into_kind(r#" '' "#),