just playing with tangled
at globpattern 737 lines 25 kB view raw
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_tuple().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_tuple().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_tuple().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_tuple().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}