1use 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
78pub type FilesetDiagnostics = Diagnostics<FilesetParseError>;
81
82pub type FilesetParseResult<T> = Result<T, FilesetParseError>;
84
85#[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#[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 pub(super) fn expression(message: impl Into<String>, span: pest::Span<'_>) -> Self {
135 FilesetParseError::new(FilesetParseErrorKind::Expression(message.into()), span)
136 }
137
138 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 UnionAll(Vec<ExpressionNode<'i>>),
184 FunctionCall(Box<FunctionCallNode<'i>>),
185}
186
187#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
188pub enum UnaryOp {
189 Negate,
191}
192
193#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
194pub enum BinaryOp {
195 Intersection,
197 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 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![], 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
316pub 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
323pub 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 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 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 assert_eq!(
494 parse_into_kind(r#" "\y" "#),
495 Err(FilesetParseErrorKind::SyntaxError),
496 );
497
498 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 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 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 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 assert!(parse_into_kind("foo(,)").is_err());
639
640 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 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 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 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 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}