jj_lib/
fileset_parser.rs

1// Copyright 2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Parser for the fileset language.
16
17use std::error;
18use std::sync::LazyLock;
19
20use itertools::Itertools as _;
21use pest::Parser as _;
22use pest::iterators::Pair;
23use pest::pratt_parser::Assoc;
24use pest::pratt_parser::Op;
25use pest::pratt_parser::PrattParser;
26use pest_derive::Parser;
27use thiserror::Error;
28
29use crate::dsl_util;
30use crate::dsl_util::Diagnostics;
31use crate::dsl_util::InvalidArguments;
32use crate::dsl_util::StringLiteralParser;
33
34#[derive(Parser)]
35#[grammar = "fileset.pest"]
36struct FilesetParser;
37
38const STRING_LITERAL_PARSER: StringLiteralParser<Rule> = StringLiteralParser {
39    content_rule: Rule::string_content,
40    escape_rule: Rule::string_escape,
41};
42
43impl Rule {
44    fn to_symbol(self) -> Option<&'static str> {
45        match self {
46            Self::EOI => None,
47            Self::whitespace => None,
48            Self::identifier => None,
49            Self::strict_identifier_part => None,
50            Self::strict_identifier => None,
51            Self::bare_string => None,
52            Self::string_escape => None,
53            Self::string_content_char => None,
54            Self::string_content => None,
55            Self::string_literal => None,
56            Self::raw_string_content => None,
57            Self::raw_string_literal => None,
58            Self::pattern_kind_op => Some(":"),
59            Self::negate_op => Some("~"),
60            Self::union_op => Some("|"),
61            Self::intersection_op => Some("&"),
62            Self::difference_op => Some("~"),
63            Self::prefix_ops => None,
64            Self::infix_ops => None,
65            Self::function => None,
66            Self::function_name => None,
67            Self::function_arguments => None,
68            Self::string_pattern => None,
69            Self::bare_string_pattern => None,
70            Self::primary => None,
71            Self::expression => None,
72            Self::program => None,
73            Self::program_or_bare_string => None,
74        }
75    }
76}
77
78/// Manages diagnostic messages emitted during fileset parsing and name
79/// resolution.
80pub type FilesetDiagnostics = Diagnostics<FilesetParseError>;
81
82/// Result of fileset parsing and name resolution.
83pub type FilesetParseResult<T> = Result<T, FilesetParseError>;
84
85/// Error occurred during fileset parsing and name resolution.
86#[derive(Debug, Error)]
87#[error("{pest_error}")]
88pub struct FilesetParseError {
89    kind: FilesetParseErrorKind,
90    pest_error: Box<pest::error::Error<Rule>>,
91    source: Option<Box<dyn error::Error + Send + Sync>>,
92}
93
94/// Categories of fileset parsing and name resolution error.
95#[expect(missing_docs)]
96#[derive(Clone, Debug, Eq, Error, PartialEq)]
97pub enum FilesetParseErrorKind {
98    #[error("Syntax error")]
99    SyntaxError,
100    #[error("Function `{name}` doesn't exist")]
101    NoSuchFunction {
102        name: String,
103        candidates: Vec<String>,
104    },
105    #[error("Function `{name}`: {message}")]
106    InvalidArguments { name: String, message: String },
107    #[error("{0}")]
108    Expression(String),
109}
110
111impl FilesetParseError {
112    pub(super) fn new(kind: FilesetParseErrorKind, span: pest::Span<'_>) -> Self {
113        let message = kind.to_string();
114        let pest_error = Box::new(pest::error::Error::new_from_span(
115            pest::error::ErrorVariant::CustomError { message },
116            span,
117        ));
118        Self {
119            kind,
120            pest_error,
121            source: None,
122        }
123    }
124
125    pub(super) fn with_source(
126        mut self,
127        source: impl Into<Box<dyn error::Error + Send + Sync>>,
128    ) -> Self {
129        self.source = Some(source.into());
130        self
131    }
132
133    /// Some other expression error.
134    pub(super) fn expression(message: impl Into<String>, span: pest::Span<'_>) -> Self {
135        Self::new(FilesetParseErrorKind::Expression(message.into()), span)
136    }
137
138    /// Category of the underlying error.
139    pub fn kind(&self) -> &FilesetParseErrorKind {
140        &self.kind
141    }
142}
143
144impl From<pest::error::Error<Rule>> for FilesetParseError {
145    fn from(err: pest::error::Error<Rule>) -> Self {
146        Self {
147            kind: FilesetParseErrorKind::SyntaxError,
148            pest_error: Box::new(rename_rules_in_pest_error(err)),
149            source: None,
150        }
151    }
152}
153
154impl From<InvalidArguments<'_>> for FilesetParseError {
155    fn from(err: InvalidArguments<'_>) -> Self {
156        let kind = FilesetParseErrorKind::InvalidArguments {
157            name: err.name.to_owned(),
158            message: err.message,
159        };
160        Self::new(kind, err.span)
161    }
162}
163
164fn rename_rules_in_pest_error(err: pest::error::Error<Rule>) -> pest::error::Error<Rule> {
165    err.renamed_rules(|rule| {
166        rule.to_symbol()
167            .map(|sym| format!("`{sym}`"))
168            .unwrap_or_else(|| format!("<{rule:?}>"))
169    })
170}
171
172#[derive(Clone, Debug, Eq, PartialEq)]
173pub enum ExpressionKind<'i> {
174    Identifier(&'i str),
175    String(String),
176    StringPattern {
177        kind: &'i str,
178        value: String,
179    },
180    Unary(UnaryOp, Box<ExpressionNode<'i>>),
181    Binary(BinaryOp, Box<ExpressionNode<'i>>, Box<ExpressionNode<'i>>),
182    /// `x | y | ..`
183    UnionAll(Vec<ExpressionNode<'i>>),
184    FunctionCall(Box<FunctionCallNode<'i>>),
185}
186
187#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
188pub enum UnaryOp {
189    /// `~`
190    Negate,
191}
192
193#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
194pub enum BinaryOp {
195    /// `&`
196    Intersection,
197    /// `~`
198    Difference,
199}
200
201pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>;
202pub type FunctionCallNode<'i> = dsl_util::FunctionCallNode<'i, ExpressionKind<'i>>;
203
204fn union_nodes<'i>(lhs: ExpressionNode<'i>, rhs: ExpressionNode<'i>) -> ExpressionNode<'i> {
205    let span = lhs.span.start_pos().span(&rhs.span.end_pos());
206    let expr = match lhs.kind {
207        // Flatten "x | y | z" to save recursion stack. Machine-generated query
208        // might have long chain of unions.
209        ExpressionKind::UnionAll(mut nodes) => {
210            nodes.push(rhs);
211            ExpressionKind::UnionAll(nodes)
212        }
213        _ => ExpressionKind::UnionAll(vec![lhs, rhs]),
214    };
215    ExpressionNode::new(expr, span)
216}
217
218fn parse_function_call_node(pair: Pair<Rule>) -> FilesetParseResult<FunctionCallNode> {
219    assert_eq!(pair.as_rule(), Rule::function);
220    let [name_pair, args_pair] = pair.into_inner().collect_array().unwrap();
221    assert_eq!(name_pair.as_rule(), Rule::function_name);
222    assert_eq!(args_pair.as_rule(), Rule::function_arguments);
223    let name_span = name_pair.as_span();
224    let args_span = args_pair.as_span();
225    let name = name_pair.as_str();
226    let args = args_pair
227        .into_inner()
228        .map(parse_expression_node)
229        .try_collect()?;
230    Ok(FunctionCallNode {
231        name,
232        name_span,
233        args,
234        keyword_args: vec![], // unsupported
235        args_span,
236    })
237}
238
239fn parse_as_string_literal(pair: Pair<Rule>) -> String {
240    match pair.as_rule() {
241        Rule::identifier => pair.as_str().to_owned(),
242        Rule::string_literal => STRING_LITERAL_PARSER.parse(pair.into_inner()),
243        Rule::raw_string_literal => {
244            let [content] = pair.into_inner().collect_array().unwrap();
245            assert_eq!(content.as_rule(), Rule::raw_string_content);
246            content.as_str().to_owned()
247        }
248        r => panic!("unexpected string literal rule: {r:?}"),
249    }
250}
251
252fn parse_primary_node(pair: Pair<Rule>) -> FilesetParseResult<ExpressionNode> {
253    assert_eq!(pair.as_rule(), Rule::primary);
254    let span = pair.as_span();
255    let first = pair.into_inner().next().unwrap();
256    let expr = match first.as_rule() {
257        // Ignore inner span to preserve parenthesized expression as such.
258        Rule::expression => parse_expression_node(first)?.kind,
259        Rule::function => {
260            let function = Box::new(parse_function_call_node(first)?);
261            ExpressionKind::FunctionCall(function)
262        }
263        Rule::string_pattern => {
264            let [lhs, op, rhs] = first.into_inner().collect_array().unwrap();
265            assert_eq!(lhs.as_rule(), Rule::strict_identifier);
266            assert_eq!(op.as_rule(), Rule::pattern_kind_op);
267            let kind = lhs.as_str();
268            let value = parse_as_string_literal(rhs);
269            ExpressionKind::StringPattern { kind, value }
270        }
271        Rule::identifier => ExpressionKind::Identifier(first.as_str()),
272        Rule::string_literal | Rule::raw_string_literal => {
273            ExpressionKind::String(parse_as_string_literal(first))
274        }
275        r => panic!("unexpected primary rule: {r:?}"),
276    };
277    Ok(ExpressionNode::new(expr, span))
278}
279
280fn parse_expression_node(pair: Pair<Rule>) -> FilesetParseResult<ExpressionNode> {
281    assert_eq!(pair.as_rule(), Rule::expression);
282    static PRATT: LazyLock<PrattParser<Rule>> = LazyLock::new(|| {
283        PrattParser::new()
284            .op(Op::infix(Rule::union_op, Assoc::Left))
285            .op(Op::infix(Rule::intersection_op, Assoc::Left)
286                | Op::infix(Rule::difference_op, Assoc::Left))
287            .op(Op::prefix(Rule::negate_op))
288    });
289    PRATT
290        .map_primary(parse_primary_node)
291        .map_prefix(|op, rhs| {
292            let op_kind = match op.as_rule() {
293                Rule::negate_op => UnaryOp::Negate,
294                r => panic!("unexpected prefix operator rule {r:?}"),
295            };
296            let rhs = Box::new(rhs?);
297            let span = op.as_span().start_pos().span(&rhs.span.end_pos());
298            let expr = ExpressionKind::Unary(op_kind, rhs);
299            Ok(ExpressionNode::new(expr, span))
300        })
301        .map_infix(|lhs, op, rhs| {
302            let op_kind = match op.as_rule() {
303                Rule::union_op => return Ok(union_nodes(lhs?, rhs?)),
304                Rule::intersection_op => BinaryOp::Intersection,
305                Rule::difference_op => BinaryOp::Difference,
306                r => panic!("unexpected infix operator rule {r:?}"),
307            };
308            let lhs = Box::new(lhs?);
309            let rhs = Box::new(rhs?);
310            let span = lhs.span.start_pos().span(&rhs.span.end_pos());
311            let expr = ExpressionKind::Binary(op_kind, lhs, rhs);
312            Ok(ExpressionNode::new(expr, span))
313        })
314        .parse(pair.into_inner())
315}
316
317/// Parses text into expression tree. No name resolution is made at this stage.
318pub fn parse_program(text: &str) -> FilesetParseResult<ExpressionNode<'_>> {
319    let mut pairs = FilesetParser::parse(Rule::program, text)?;
320    let first = pairs.next().unwrap();
321    parse_expression_node(first)
322}
323
324/// Parses text into expression tree with bare string fallback. No name
325/// resolution is made at this stage.
326///
327/// If the text can't be parsed as a fileset expression, and if it doesn't
328/// contain any operator-like characters, it will be parsed as a file path.
329pub fn parse_program_or_bare_string(text: &str) -> FilesetParseResult<ExpressionNode<'_>> {
330    let mut pairs = FilesetParser::parse(Rule::program_or_bare_string, text)?;
331    let first = pairs.next().unwrap();
332    let span = first.as_span();
333    let expr = match first.as_rule() {
334        Rule::expression => return parse_expression_node(first),
335        Rule::bare_string_pattern => {
336            let [lhs, op, rhs] = first.into_inner().collect_array().unwrap();
337            assert_eq!(lhs.as_rule(), Rule::strict_identifier);
338            assert_eq!(op.as_rule(), Rule::pattern_kind_op);
339            assert_eq!(rhs.as_rule(), Rule::bare_string);
340            let kind = lhs.as_str();
341            let value = rhs.as_str().to_owned();
342            ExpressionKind::StringPattern { kind, value }
343        }
344        Rule::bare_string => ExpressionKind::String(first.as_str().to_owned()),
345        r => panic!("unexpected program or bare string rule: {r:?}"),
346    };
347    Ok(ExpressionNode::new(expr, span))
348}
349
350#[cfg(test)]
351mod tests {
352    use assert_matches::assert_matches;
353
354    use super::*;
355    use crate::dsl_util::KeywordArgument;
356
357    fn parse_into_kind(text: &str) -> Result<ExpressionKind<'_>, FilesetParseErrorKind> {
358        parse_program(text)
359            .map(|node| node.kind)
360            .map_err(|err| err.kind)
361    }
362
363    fn parse_maybe_bare_into_kind(text: &str) -> Result<ExpressionKind<'_>, FilesetParseErrorKind> {
364        parse_program_or_bare_string(text)
365            .map(|node| node.kind)
366            .map_err(|err| err.kind)
367    }
368
369    fn parse_normalized(text: &str) -> ExpressionNode<'_> {
370        normalize_tree(parse_program(text).unwrap())
371    }
372
373    fn parse_maybe_bare_normalized(text: &str) -> ExpressionNode<'_> {
374        normalize_tree(parse_program_or_bare_string(text).unwrap())
375    }
376
377    /// Drops auxiliary data from parsed tree so it can be compared with other.
378    fn normalize_tree(node: ExpressionNode) -> ExpressionNode {
379        fn empty_span() -> pest::Span<'static> {
380            pest::Span::new("", 0, 0).unwrap()
381        }
382
383        fn normalize_list(nodes: Vec<ExpressionNode>) -> Vec<ExpressionNode> {
384            nodes.into_iter().map(normalize_tree).collect()
385        }
386
387        fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode {
388            FunctionCallNode {
389                name: function.name,
390                name_span: empty_span(),
391                args: normalize_list(function.args),
392                keyword_args: function
393                    .keyword_args
394                    .into_iter()
395                    .map(|arg| KeywordArgument {
396                        name: arg.name,
397                        name_span: empty_span(),
398                        value: normalize_tree(arg.value),
399                    })
400                    .collect(),
401                args_span: empty_span(),
402            }
403        }
404
405        let normalized_kind = match node.kind {
406            ExpressionKind::Identifier(_)
407            | ExpressionKind::String(_)
408            | ExpressionKind::StringPattern { .. } => node.kind,
409            ExpressionKind::Unary(op, arg) => {
410                let arg = Box::new(normalize_tree(*arg));
411                ExpressionKind::Unary(op, arg)
412            }
413            ExpressionKind::Binary(op, lhs, rhs) => {
414                let lhs = Box::new(normalize_tree(*lhs));
415                let rhs = Box::new(normalize_tree(*rhs));
416                ExpressionKind::Binary(op, lhs, rhs)
417            }
418            ExpressionKind::UnionAll(nodes) => {
419                let nodes = normalize_list(nodes);
420                ExpressionKind::UnionAll(nodes)
421            }
422            ExpressionKind::FunctionCall(function) => {
423                let function = Box::new(normalize_function_call(*function));
424                ExpressionKind::FunctionCall(function)
425            }
426        };
427        ExpressionNode {
428            kind: normalized_kind,
429            span: empty_span(),
430        }
431    }
432
433    #[test]
434    fn test_parse_tree_eq() {
435        assert_eq!(
436            parse_normalized(r#" foo( x ) | ~bar:"baz" "#),
437            parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#)
438        );
439        assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#));
440    }
441
442    #[test]
443    fn test_parse_invalid_function_name() {
444        assert_eq!(
445            parse_into_kind("5foo(x)"),
446            Err(FilesetParseErrorKind::SyntaxError)
447        );
448    }
449
450    #[test]
451    fn test_parse_whitespace() {
452        let ascii_whitespaces: String = ('\x00'..='\x7f')
453            .filter(char::is_ascii_whitespace)
454            .collect();
455        assert_eq!(
456            parse_normalized(&format!("{ascii_whitespaces}f()")),
457            parse_normalized("f()")
458        );
459    }
460
461    #[test]
462    fn test_parse_identifier() {
463        assert_eq!(
464            parse_into_kind("dir/foo-bar_0.baz"),
465            Ok(ExpressionKind::Identifier("dir/foo-bar_0.baz"))
466        );
467        assert_eq!(
468            parse_into_kind("cli-reference@.md.snap"),
469            Ok(ExpressionKind::Identifier("cli-reference@.md.snap"))
470        );
471        assert_eq!(
472            parse_into_kind("柔術.jj"),
473            Ok(ExpressionKind::Identifier("柔術.jj"))
474        );
475        assert_eq!(
476            parse_into_kind(r#"Windows\Path"#),
477            Ok(ExpressionKind::Identifier(r#"Windows\Path"#))
478        );
479        assert_eq!(
480            parse_into_kind("glob*[chars]?"),
481            Ok(ExpressionKind::Identifier("glob*[chars]?"))
482        );
483    }
484
485    #[test]
486    fn test_parse_string_literal() {
487        // "\<char>" escapes
488        assert_eq!(
489            parse_into_kind(r#" "\t\r\n\"\\\0\e" "#),
490            Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())),
491        );
492
493        // Invalid "\<char>" escape
494        assert_eq!(
495            parse_into_kind(r#" "\y" "#),
496            Err(FilesetParseErrorKind::SyntaxError),
497        );
498
499        // Single-quoted raw string
500        assert_eq!(
501            parse_into_kind(r#" '' "#),
502            Ok(ExpressionKind::String("".to_owned())),
503        );
504        assert_eq!(
505            parse_into_kind(r#" 'a\n' "#),
506            Ok(ExpressionKind::String(r"a\n".to_owned())),
507        );
508        assert_eq!(
509            parse_into_kind(r#" '\' "#),
510            Ok(ExpressionKind::String(r"\".to_owned())),
511        );
512        assert_eq!(
513            parse_into_kind(r#" '"' "#),
514            Ok(ExpressionKind::String(r#"""#.to_owned())),
515        );
516
517        // Hex bytes
518        assert_eq!(
519            parse_into_kind(r#""\x61\x65\x69\x6f\x75""#),
520            Ok(ExpressionKind::String("aeiou".to_owned())),
521        );
522        assert_eq!(
523            parse_into_kind(r#""\xe0\xe8\xec\xf0\xf9""#),
524            Ok(ExpressionKind::String("àèìðù".to_owned())),
525        );
526        assert_eq!(
527            parse_into_kind(r#""\x""#),
528            Err(FilesetParseErrorKind::SyntaxError),
529        );
530        assert_eq!(
531            parse_into_kind(r#""\xf""#),
532            Err(FilesetParseErrorKind::SyntaxError),
533        );
534        assert_eq!(
535            parse_into_kind(r#""\xgg""#),
536            Err(FilesetParseErrorKind::SyntaxError),
537        );
538    }
539
540    #[test]
541    fn test_parse_string_pattern() {
542        assert_eq!(
543            parse_into_kind(r#" foo:bar "#),
544            Ok(ExpressionKind::StringPattern {
545                kind: "foo",
546                value: "bar".to_owned()
547            })
548        );
549        assert_eq!(
550            parse_into_kind(" foo:glob*[chars]? "),
551            Ok(ExpressionKind::StringPattern {
552                kind: "foo",
553                value: "glob*[chars]?".to_owned()
554            })
555        );
556        assert_eq!(
557            parse_into_kind(r#" foo:"bar" "#),
558            Ok(ExpressionKind::StringPattern {
559                kind: "foo",
560                value: "bar".to_owned()
561            })
562        );
563        assert_eq!(
564            parse_into_kind(r#" foo:"" "#),
565            Ok(ExpressionKind::StringPattern {
566                kind: "foo",
567                value: "".to_owned()
568            })
569        );
570        assert_eq!(
571            parse_into_kind(r#" foo:'\' "#),
572            Ok(ExpressionKind::StringPattern {
573                kind: "foo",
574                value: r"\".to_owned()
575            })
576        );
577        assert_eq!(
578            parse_into_kind(r#" foo: "#),
579            Err(FilesetParseErrorKind::SyntaxError)
580        );
581        assert_eq!(
582            parse_into_kind(r#" foo: "" "#),
583            Err(FilesetParseErrorKind::SyntaxError)
584        );
585        assert_eq!(
586            parse_into_kind(r#" foo :"" "#),
587            Err(FilesetParseErrorKind::SyntaxError)
588        );
589    }
590
591    #[test]
592    fn test_parse_operator() {
593        assert_matches!(
594            parse_into_kind("~x"),
595            Ok(ExpressionKind::Unary(UnaryOp::Negate, _))
596        );
597        assert_matches!(
598            parse_into_kind("x|y"),
599            Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 2
600        );
601        assert_matches!(
602            parse_into_kind("x|y|z"),
603            Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 3
604        );
605        assert_matches!(
606            parse_into_kind("x&y"),
607            Ok(ExpressionKind::Binary(BinaryOp::Intersection, _, _))
608        );
609        assert_matches!(
610            parse_into_kind("x~y"),
611            Ok(ExpressionKind::Binary(BinaryOp::Difference, _, _))
612        );
613
614        // Set operator associativity/precedence
615        assert_eq!(parse_normalized("~x|y"), parse_normalized("(~x)|y"));
616        assert_eq!(parse_normalized("x&~y"), parse_normalized("x&(~y)"));
617        assert_eq!(parse_normalized("x~~y"), parse_normalized("x~(~y)"));
618        assert_eq!(parse_normalized("x~~~y"), parse_normalized("x~(~(~y))"));
619        assert_eq!(parse_normalized("x|y|z"), parse_normalized("(x|y)|z"));
620        assert_eq!(parse_normalized("x&y|z"), parse_normalized("(x&y)|z"));
621        assert_eq!(parse_normalized("x|y&z"), parse_normalized("x|(y&z)"));
622        assert_eq!(parse_normalized("x|y~z"), parse_normalized("x|(y~z)"));
623        assert_eq!(parse_normalized("~x:y"), parse_normalized("~(x:y)"));
624        assert_eq!(parse_normalized("x|y:z"), parse_normalized("x|(y:z)"));
625
626        // Expression span
627        assert_eq!(parse_program(" ~ x ").unwrap().span.as_str(), "~ x");
628        assert_eq!(parse_program(" x |y ").unwrap().span.as_str(), "x |y");
629        assert_eq!(parse_program(" (x) ").unwrap().span.as_str(), "(x)");
630        assert_eq!(parse_program("~( x|y) ").unwrap().span.as_str(), "~( x|y)");
631    }
632
633    #[test]
634    fn test_parse_function_call() {
635        fn unwrap_function_call(node: ExpressionNode<'_>) -> Box<FunctionCallNode<'_>> {
636            match node.kind {
637                ExpressionKind::FunctionCall(function) => function,
638                _ => panic!("unexpected expression: {node:?}"),
639            }
640        }
641
642        assert_matches!(
643            parse_into_kind("foo()"),
644            Ok(ExpressionKind::FunctionCall(_))
645        );
646
647        // Trailing comma isn't allowed for empty argument
648        assert!(parse_into_kind("foo(,)").is_err());
649
650        // Trailing comma is allowed for the last argument
651        assert_eq!(parse_normalized("foo(a,)"), parse_normalized("foo(a)"));
652        assert_eq!(parse_normalized("foo(a ,  )"), parse_normalized("foo(a)"));
653        assert!(parse_into_kind("foo(,a)").is_err());
654        assert!(parse_into_kind("foo(a,,)").is_err());
655        assert!(parse_into_kind("foo(a  , , )").is_err());
656        assert_eq!(parse_normalized("foo(a,b,)"), parse_normalized("foo(a,b)"));
657        assert!(parse_into_kind("foo(a,,b)").is_err());
658
659        // Expression span
660        let function = unwrap_function_call(parse_program("foo( a, (b) , ~(c) )").unwrap());
661        assert_eq!(function.name_span.as_str(), "foo");
662        assert_eq!(function.args_span.as_str(), "a, (b) , ~(c)");
663        assert_eq!(function.args[0].span.as_str(), "a");
664        assert_eq!(function.args[1].span.as_str(), "(b)");
665        assert_eq!(function.args[2].span.as_str(), "~(c)");
666    }
667
668    #[test]
669    fn test_parse_bare_string() {
670        // Valid expression should be parsed as such
671        assert_eq!(
672            parse_maybe_bare_into_kind(" valid "),
673            Ok(ExpressionKind::Identifier("valid"))
674        );
675        assert_eq!(
676            parse_maybe_bare_normalized("f(x)&y"),
677            parse_normalized("f(x)&y")
678        );
679
680        // Bare string
681        assert_eq!(
682            parse_maybe_bare_into_kind("Foo Bar.txt"),
683            Ok(ExpressionKind::String("Foo Bar.txt".to_owned()))
684        );
685        assert_eq!(
686            parse_maybe_bare_into_kind(r#"Windows\Path with space"#),
687            Ok(ExpressionKind::String(
688                r#"Windows\Path with space"#.to_owned()
689            ))
690        );
691        assert_eq!(
692            parse_maybe_bare_into_kind("柔 術 . j j"),
693            Ok(ExpressionKind::String("柔 術 . j j".to_owned()))
694        );
695        assert_eq!(
696            parse_maybe_bare_into_kind("Unicode emoji 💩"),
697            Ok(ExpressionKind::String("Unicode emoji 💩".to_owned()))
698        );
699        assert_eq!(
700            parse_maybe_bare_into_kind("looks like & expression"),
701            Err(FilesetParseErrorKind::SyntaxError)
702        );
703        assert_eq!(
704            parse_maybe_bare_into_kind("unbalanced_parens("),
705            Err(FilesetParseErrorKind::SyntaxError)
706        );
707
708        // Bare string pattern
709        assert_eq!(
710            parse_maybe_bare_into_kind("foo: bar baz"),
711            Ok(ExpressionKind::StringPattern {
712                kind: "foo",
713                value: " bar baz".to_owned()
714            })
715        );
716        assert_eq!(
717            parse_maybe_bare_into_kind("foo:glob * [chars]?"),
718            Ok(ExpressionKind::StringPattern {
719                kind: "foo",
720                value: "glob * [chars]?".to_owned()
721            })
722        );
723        assert_eq!(
724            parse_maybe_bare_into_kind("foo:bar:baz"),
725            Err(FilesetParseErrorKind::SyntaxError)
726        );
727        assert_eq!(
728            parse_maybe_bare_into_kind("foo:"),
729            Err(FilesetParseErrorKind::SyntaxError)
730        );
731        assert_eq!(
732            parse_maybe_bare_into_kind(r#"foo:"unclosed quote"#),
733            Err(FilesetParseErrorKind::SyntaxError)
734        );
735
736        // Surrounding spaces are simply preserved. They could be trimmed, but
737        // space is valid bare_string character.
738        assert_eq!(
739            parse_maybe_bare_into_kind(" No trim "),
740            Ok(ExpressionKind::String(" No trim ".to_owned()))
741        );
742    }
743
744    #[test]
745    fn test_parse_error() {
746        insta::assert_snapshot!(parse_program("foo|").unwrap_err().to_string(), @r"
747         --> 1:5
748          |
749        1 | foo|
750          |     ^---
751          |
752          = expected `~` or <primary>
753        ");
754    }
755}