1use 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
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 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 pub(super) fn expression(message: impl Into<String>, span: pest::Span<'_>) -> Self {
135 Self::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 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 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 span = pair.as_span();
255 let first = pair.into_inner().next().unwrap();
256 let expr = match first.as_rule() {
257 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
317pub 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
324pub 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 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 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 assert_eq!(
495 parse_into_kind(r#" "\y" "#),
496 Err(FilesetParseErrorKind::SyntaxError),
497 );
498
499 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 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 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 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 assert!(parse_into_kind("foo(,)").is_err());
649
650 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 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 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 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 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 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}