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;
18
19use itertools::Itertools as _;
20use once_cell::sync::Lazy;
21use pest::iterators::Pair;
22use pest::pratt_parser::Assoc;
23use pest::pratt_parser::Op;
24use pest::pratt_parser::PrattParser;
25use pest::Parser as _;
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            Rule::EOI => None,
47            Rule::whitespace => None,
48            Rule::identifier => None,
49            Rule::strict_identifier_part => None,
50            Rule::strict_identifier => None,
51            Rule::bare_string => None,
52            Rule::string_escape => None,
53            Rule::string_content_char => None,
54            Rule::string_content => None,
55            Rule::string_literal => None,
56            Rule::raw_string_content => None,
57            Rule::raw_string_literal => None,
58            Rule::pattern_kind_op => Some(":"),
59            Rule::negate_op => Some("~"),
60            Rule::union_op => Some("|"),
61            Rule::intersection_op => Some("&"),
62            Rule::difference_op => Some("~"),
63            Rule::prefix_ops => None,
64            Rule::infix_ops => None,
65            Rule::function => None,
66            Rule::function_name => None,
67            Rule::function_arguments => None,
68            Rule::string_pattern => None,
69            Rule::bare_string_pattern => None,
70            Rule::primary => None,
71            Rule::expression => None,
72            Rule::program => None,
73            Rule::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        FilesetParseError {
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        FilesetParseError::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        FilesetParseError {
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 first = pair.into_inner().next().unwrap();
255    let span = first.as_span();
256    let expr = match first.as_rule() {
257        Rule::expression => return parse_expression_node(first),
258        Rule::function => {
259            let function = Box::new(parse_function_call_node(first)?);
260            ExpressionKind::FunctionCall(function)
261        }
262        Rule::string_pattern => {
263            let [lhs, op, rhs] = first.into_inner().collect_array().unwrap();
264            assert_eq!(lhs.as_rule(), Rule::strict_identifier);
265            assert_eq!(op.as_rule(), Rule::pattern_kind_op);
266            let kind = lhs.as_str();
267            let value = parse_as_string_literal(rhs);
268            ExpressionKind::StringPattern { kind, value }
269        }
270        Rule::identifier => ExpressionKind::Identifier(first.as_str()),
271        Rule::string_literal | Rule::raw_string_literal => {
272            ExpressionKind::String(parse_as_string_literal(first))
273        }
274        r => panic!("unexpected primary rule: {r:?}"),
275    };
276    Ok(ExpressionNode::new(expr, span))
277}
278
279fn parse_expression_node(pair: Pair<Rule>) -> FilesetParseResult<ExpressionNode> {
280    assert_eq!(pair.as_rule(), Rule::expression);
281    static PRATT: Lazy<PrattParser<Rule>> = Lazy::new(|| {
282        PrattParser::new()
283            .op(Op::infix(Rule::union_op, Assoc::Left))
284            .op(Op::infix(Rule::intersection_op, Assoc::Left)
285                | Op::infix(Rule::difference_op, Assoc::Left))
286            .op(Op::prefix(Rule::negate_op))
287    });
288    PRATT
289        .map_primary(parse_primary_node)
290        .map_prefix(|op, rhs| {
291            let op_kind = match op.as_rule() {
292                Rule::negate_op => UnaryOp::Negate,
293                r => panic!("unexpected prefix operator rule {r:?}"),
294            };
295            let rhs = Box::new(rhs?);
296            let span = op.as_span().start_pos().span(&rhs.span.end_pos());
297            let expr = ExpressionKind::Unary(op_kind, rhs);
298            Ok(ExpressionNode::new(expr, span))
299        })
300        .map_infix(|lhs, op, rhs| {
301            let op_kind = match op.as_rule() {
302                Rule::union_op => return Ok(union_nodes(lhs?, rhs?)),
303                Rule::intersection_op => BinaryOp::Intersection,
304                Rule::difference_op => BinaryOp::Difference,
305                r => panic!("unexpected infix operator rule {r:?}"),
306            };
307            let lhs = Box::new(lhs?);
308            let rhs = Box::new(rhs?);
309            let span = lhs.span.start_pos().span(&rhs.span.end_pos());
310            let expr = ExpressionKind::Binary(op_kind, lhs, rhs);
311            Ok(ExpressionNode::new(expr, span))
312        })
313        .parse(pair.into_inner())
314}
315
316/// Parses text into expression tree. No name resolution is made at this stage.
317pub fn parse_program(text: &str) -> FilesetParseResult<ExpressionNode> {
318    let mut pairs = FilesetParser::parse(Rule::program, text)?;
319    let first = pairs.next().unwrap();
320    parse_expression_node(first)
321}
322
323/// Parses text into expression tree with bare string fallback. No name
324/// resolution is made at this stage.
325///
326/// If the text can't be parsed as a fileset expression, and if it doesn't
327/// contain any operator-like characters, it will be parsed as a file path.
328pub fn parse_program_or_bare_string(text: &str) -> FilesetParseResult<ExpressionNode> {
329    let mut pairs = FilesetParser::parse(Rule::program_or_bare_string, text)?;
330    let first = pairs.next().unwrap();
331    let span = first.as_span();
332    let expr = match first.as_rule() {
333        Rule::expression => return parse_expression_node(first),
334        Rule::bare_string_pattern => {
335            let [lhs, op, rhs] = first.into_inner().collect_array().unwrap();
336            assert_eq!(lhs.as_rule(), Rule::strict_identifier);
337            assert_eq!(op.as_rule(), Rule::pattern_kind_op);
338            assert_eq!(rhs.as_rule(), Rule::bare_string);
339            let kind = lhs.as_str();
340            let value = rhs.as_str().to_owned();
341            ExpressionKind::StringPattern { kind, value }
342        }
343        Rule::bare_string => ExpressionKind::String(first.as_str().to_owned()),
344        r => panic!("unexpected program or bare string rule: {r:?}"),
345    };
346    Ok(ExpressionNode::new(expr, span))
347}
348
349#[cfg(test)]
350mod tests {
351    use assert_matches::assert_matches;
352
353    use super::*;
354    use crate::dsl_util::KeywordArgument;
355
356    fn parse_into_kind(text: &str) -> Result<ExpressionKind, FilesetParseErrorKind> {
357        parse_program(text)
358            .map(|node| node.kind)
359            .map_err(|err| err.kind)
360    }
361
362    fn parse_maybe_bare_into_kind(text: &str) -> Result<ExpressionKind, FilesetParseErrorKind> {
363        parse_program_or_bare_string(text)
364            .map(|node| node.kind)
365            .map_err(|err| err.kind)
366    }
367
368    fn parse_normalized(text: &str) -> ExpressionNode {
369        normalize_tree(parse_program(text).unwrap())
370    }
371
372    fn parse_maybe_bare_normalized(text: &str) -> ExpressionNode {
373        normalize_tree(parse_program_or_bare_string(text).unwrap())
374    }
375
376    /// Drops auxiliary data from parsed tree so it can be compared with other.
377    fn normalize_tree(node: ExpressionNode) -> ExpressionNode {
378        fn empty_span() -> pest::Span<'static> {
379            pest::Span::new("", 0, 0).unwrap()
380        }
381
382        fn normalize_list(nodes: Vec<ExpressionNode>) -> Vec<ExpressionNode> {
383            nodes.into_iter().map(normalize_tree).collect()
384        }
385
386        fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode {
387            FunctionCallNode {
388                name: function.name,
389                name_span: empty_span(),
390                args: normalize_list(function.args),
391                keyword_args: function
392                    .keyword_args
393                    .into_iter()
394                    .map(|arg| KeywordArgument {
395                        name: arg.name,
396                        name_span: empty_span(),
397                        value: normalize_tree(arg.value),
398                    })
399                    .collect(),
400                args_span: empty_span(),
401            }
402        }
403
404        let normalized_kind = match node.kind {
405            ExpressionKind::Identifier(_)
406            | ExpressionKind::String(_)
407            | ExpressionKind::StringPattern { .. } => node.kind,
408            ExpressionKind::Unary(op, arg) => {
409                let arg = Box::new(normalize_tree(*arg));
410                ExpressionKind::Unary(op, arg)
411            }
412            ExpressionKind::Binary(op, lhs, rhs) => {
413                let lhs = Box::new(normalize_tree(*lhs));
414                let rhs = Box::new(normalize_tree(*rhs));
415                ExpressionKind::Binary(op, lhs, rhs)
416            }
417            ExpressionKind::UnionAll(nodes) => {
418                let nodes = normalize_list(nodes);
419                ExpressionKind::UnionAll(nodes)
420            }
421            ExpressionKind::FunctionCall(function) => {
422                let function = Box::new(normalize_function_call(*function));
423                ExpressionKind::FunctionCall(function)
424            }
425        };
426        ExpressionNode {
427            kind: normalized_kind,
428            span: empty_span(),
429        }
430    }
431
432    #[test]
433    fn test_parse_tree_eq() {
434        assert_eq!(
435            parse_normalized(r#" foo( x ) | ~bar:"baz" "#),
436            parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#)
437        );
438        assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#));
439    }
440
441    #[test]
442    fn test_parse_invalid_function_name() {
443        assert_eq!(
444            parse_into_kind("5foo(x)"),
445            Err(FilesetParseErrorKind::SyntaxError)
446        );
447    }
448
449    #[test]
450    fn test_parse_whitespace() {
451        let ascii_whitespaces: String = ('\x00'..='\x7f')
452            .filter(char::is_ascii_whitespace)
453            .collect();
454        assert_eq!(
455            parse_normalized(&format!("{ascii_whitespaces}f()")),
456            parse_normalized("f()")
457        );
458    }
459
460    #[test]
461    fn test_parse_identifier() {
462        assert_eq!(
463            parse_into_kind("dir/foo-bar_0.baz"),
464            Ok(ExpressionKind::Identifier("dir/foo-bar_0.baz"))
465        );
466        assert_eq!(
467            parse_into_kind("cli-reference@.md.snap"),
468            Ok(ExpressionKind::Identifier("cli-reference@.md.snap"))
469        );
470        assert_eq!(
471            parse_into_kind("柔術.jj"),
472            Ok(ExpressionKind::Identifier("柔術.jj"))
473        );
474        assert_eq!(
475            parse_into_kind(r#"Windows\Path"#),
476            Ok(ExpressionKind::Identifier(r#"Windows\Path"#))
477        );
478        assert_eq!(
479            parse_into_kind("glob*[chars]?"),
480            Ok(ExpressionKind::Identifier("glob*[chars]?"))
481        );
482    }
483
484    #[test]
485    fn test_parse_string_literal() {
486        // "\<char>" escapes
487        assert_eq!(
488            parse_into_kind(r#" "\t\r\n\"\\\0\e" "#),
489            Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())),
490        );
491
492        // Invalid "\<char>" escape
493        assert_eq!(
494            parse_into_kind(r#" "\y" "#),
495            Err(FilesetParseErrorKind::SyntaxError),
496        );
497
498        // Single-quoted raw string
499        assert_eq!(
500            parse_into_kind(r#" '' "#),
501            Ok(ExpressionKind::String("".to_owned())),
502        );
503        assert_eq!(
504            parse_into_kind(r#" 'a\n' "#),
505            Ok(ExpressionKind::String(r"a\n".to_owned())),
506        );
507        assert_eq!(
508            parse_into_kind(r#" '\' "#),
509            Ok(ExpressionKind::String(r"\".to_owned())),
510        );
511        assert_eq!(
512            parse_into_kind(r#" '"' "#),
513            Ok(ExpressionKind::String(r#"""#.to_owned())),
514        );
515
516        // Hex bytes
517        assert_eq!(
518            parse_into_kind(r#""\x61\x65\x69\x6f\x75""#),
519            Ok(ExpressionKind::String("aeiou".to_owned())),
520        );
521        assert_eq!(
522            parse_into_kind(r#""\xe0\xe8\xec\xf0\xf9""#),
523            Ok(ExpressionKind::String("àèìðù".to_owned())),
524        );
525        assert_eq!(
526            parse_into_kind(r#""\x""#),
527            Err(FilesetParseErrorKind::SyntaxError),
528        );
529        assert_eq!(
530            parse_into_kind(r#""\xf""#),
531            Err(FilesetParseErrorKind::SyntaxError),
532        );
533        assert_eq!(
534            parse_into_kind(r#""\xgg""#),
535            Err(FilesetParseErrorKind::SyntaxError),
536        );
537    }
538
539    #[test]
540    fn test_parse_string_pattern() {
541        assert_eq!(
542            parse_into_kind(r#" foo:bar "#),
543            Ok(ExpressionKind::StringPattern {
544                kind: "foo",
545                value: "bar".to_owned()
546            })
547        );
548        assert_eq!(
549            parse_into_kind(" foo:glob*[chars]? "),
550            Ok(ExpressionKind::StringPattern {
551                kind: "foo",
552                value: "glob*[chars]?".to_owned()
553            })
554        );
555        assert_eq!(
556            parse_into_kind(r#" foo:"bar" "#),
557            Ok(ExpressionKind::StringPattern {
558                kind: "foo",
559                value: "bar".to_owned()
560            })
561        );
562        assert_eq!(
563            parse_into_kind(r#" foo:"" "#),
564            Ok(ExpressionKind::StringPattern {
565                kind: "foo",
566                value: "".to_owned()
567            })
568        );
569        assert_eq!(
570            parse_into_kind(r#" foo:'\' "#),
571            Ok(ExpressionKind::StringPattern {
572                kind: "foo",
573                value: r"\".to_owned()
574            })
575        );
576        assert_eq!(
577            parse_into_kind(r#" foo: "#),
578            Err(FilesetParseErrorKind::SyntaxError)
579        );
580        assert_eq!(
581            parse_into_kind(r#" foo: "" "#),
582            Err(FilesetParseErrorKind::SyntaxError)
583        );
584        assert_eq!(
585            parse_into_kind(r#" foo :"" "#),
586            Err(FilesetParseErrorKind::SyntaxError)
587        );
588    }
589
590    #[test]
591    fn test_parse_operator() {
592        assert_matches!(
593            parse_into_kind("~x"),
594            Ok(ExpressionKind::Unary(UnaryOp::Negate, _))
595        );
596        assert_matches!(
597            parse_into_kind("x|y"),
598            Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 2
599        );
600        assert_matches!(
601            parse_into_kind("x|y|z"),
602            Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 3
603        );
604        assert_matches!(
605            parse_into_kind("x&y"),
606            Ok(ExpressionKind::Binary(BinaryOp::Intersection, _, _))
607        );
608        assert_matches!(
609            parse_into_kind("x~y"),
610            Ok(ExpressionKind::Binary(BinaryOp::Difference, _, _))
611        );
612
613        // Set operator associativity/precedence
614        assert_eq!(parse_normalized("~x|y"), parse_normalized("(~x)|y"));
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|z"), parse_normalized("(x|y)|z"));
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"), parse_normalized("~(x:y)"));
623        assert_eq!(parse_normalized("x|y:z"), parse_normalized("x|(y:z)"));
624
625        // Expression span
626        assert_eq!(parse_program(" ~ x ").unwrap().span.as_str(), "~ x");
627        assert_eq!(parse_program(" x |y ").unwrap().span.as_str(), "x |y");
628    }
629
630    #[test]
631    fn test_parse_function_call() {
632        assert_matches!(
633            parse_into_kind("foo()"),
634            Ok(ExpressionKind::FunctionCall(_))
635        );
636
637        // Trailing comma isn't allowed for empty argument
638        assert!(parse_into_kind("foo(,)").is_err());
639
640        // Trailing comma is allowed for the last argument
641        assert_eq!(parse_normalized("foo(a,)"), parse_normalized("foo(a)"));
642        assert_eq!(parse_normalized("foo(a ,  )"), parse_normalized("foo(a)"));
643        assert!(parse_into_kind("foo(,a)").is_err());
644        assert!(parse_into_kind("foo(a,,)").is_err());
645        assert!(parse_into_kind("foo(a  , , )").is_err());
646        assert_eq!(parse_normalized("foo(a,b,)"), parse_normalized("foo(a,b)"));
647        assert!(parse_into_kind("foo(a,,b)").is_err());
648    }
649
650    #[test]
651    fn test_parse_bare_string() {
652        // Valid expression should be parsed as such
653        assert_eq!(
654            parse_maybe_bare_into_kind(" valid "),
655            Ok(ExpressionKind::Identifier("valid"))
656        );
657        assert_eq!(
658            parse_maybe_bare_normalized("f(x)&y"),
659            parse_normalized("f(x)&y")
660        );
661
662        // Bare string
663        assert_eq!(
664            parse_maybe_bare_into_kind("Foo Bar.txt"),
665            Ok(ExpressionKind::String("Foo Bar.txt".to_owned()))
666        );
667        assert_eq!(
668            parse_maybe_bare_into_kind(r#"Windows\Path with space"#),
669            Ok(ExpressionKind::String(
670                r#"Windows\Path with space"#.to_owned()
671            ))
672        );
673        assert_eq!(
674            parse_maybe_bare_into_kind("柔 術 . j j"),
675            Ok(ExpressionKind::String("柔 術 . j j".to_owned()))
676        );
677        assert_eq!(
678            parse_maybe_bare_into_kind("Unicode emoji 💩"),
679            Ok(ExpressionKind::String("Unicode emoji 💩".to_owned()))
680        );
681        assert_eq!(
682            parse_maybe_bare_into_kind("looks like & expression"),
683            Err(FilesetParseErrorKind::SyntaxError)
684        );
685        assert_eq!(
686            parse_maybe_bare_into_kind("unbalanced_parens("),
687            Err(FilesetParseErrorKind::SyntaxError)
688        );
689
690        // Bare string pattern
691        assert_eq!(
692            parse_maybe_bare_into_kind("foo: bar baz"),
693            Ok(ExpressionKind::StringPattern {
694                kind: "foo",
695                value: " bar baz".to_owned()
696            })
697        );
698        assert_eq!(
699            parse_maybe_bare_into_kind("foo:glob * [chars]?"),
700            Ok(ExpressionKind::StringPattern {
701                kind: "foo",
702                value: "glob * [chars]?".to_owned()
703            })
704        );
705        assert_eq!(
706            parse_maybe_bare_into_kind("foo:bar:baz"),
707            Err(FilesetParseErrorKind::SyntaxError)
708        );
709        assert_eq!(
710            parse_maybe_bare_into_kind("foo:"),
711            Err(FilesetParseErrorKind::SyntaxError)
712        );
713        assert_eq!(
714            parse_maybe_bare_into_kind(r#"foo:"unclosed quote"#),
715            Err(FilesetParseErrorKind::SyntaxError)
716        );
717
718        // Surrounding spaces are simply preserved. They could be trimmed, but
719        // space is valid bare_string character.
720        assert_eq!(
721            parse_maybe_bare_into_kind(" No trim "),
722            Ok(ExpressionKind::String(" No trim ".to_owned()))
723        );
724    }
725
726    #[test]
727    fn test_parse_error() {
728        insta::assert_snapshot!(parse_program("foo|").unwrap_err().to_string(), @r"
729         --> 1:5
730          |
731        1 | foo|
732          |     ^---
733          |
734          = expected `~` or <primary>
735        ");
736    }
737}