git_branchless_revset/parser.rs
1use lazy_static::lazy_static;
2use regex::Regex;
3use thiserror::Error;
4use tracing::instrument;
5
6use super::grammar::ExprParser;
7use super::Expr;
8
9#[derive(Debug, Error)]
10pub enum ParseError {
11 #[error("parse error: {0}")]
12 ParseError(String),
13}
14
15/// Parse a string representing a revset expression into an [Expr].
16///
17/// To update the grammar, modify `grammar.lalrpop`.
18#[instrument]
19pub fn parse(s: &str) -> Result<Expr, ParseError> {
20 ExprParser::new().parse(s).map_err(|err| {
21 let message = err.to_string();
22
23 // HACK: `lalrpop` doesn't let us customize the text of the string
24 // literal token, so replace it after the fact.
25 lazy_static! {
26 // NOTE: the `lalrpop` output contains Rust raw string literals, so
27 // we need to match those as well. However, the `#` character is
28 // interpreted by insignificant-whitespace mode as a comment, so we
29 // use `\x23` instead.
30 static ref OBJECT_RE: Regex = Regex::new(
31 r#"(?x)
32 r\x23"
33 \(
34 \[
35 [^"]+
36 "\x23
37 "#
38 )
39 .unwrap();
40 static ref STRING_LITERAL_RE: Regex = Regex::new(
41 r#"(?x)
42 r\x23"
43 \\
44 [^"]+
45 "\x23
46 "#
47 )
48 .unwrap();
49 }
50 let message = OBJECT_RE.replace(&message, "a commit/branch/tag");
51 let message = STRING_LITERAL_RE.replace(&message, "a string literal");
52
53 ParseError::ParseError(message.into_owned())
54 })
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn test_revset_parser() -> eyre::Result<()> {
63 insta::assert_debug_snapshot!(parse("hello"), @r###"
64 Ok(
65 Name(
66 "hello",
67 ),
68 )
69 "###);
70 Ok(())
71 }
72
73 #[test]
74 fn test_revset_parse_function_calls() -> eyre::Result<()> {
75 insta::assert_debug_snapshot!(parse("foo()"), @r###"
76 Ok(
77 FunctionCall(
78 "foo",
79 [],
80 ),
81 )
82 "###);
83 insta::assert_debug_snapshot!(parse("foo(bar)"), @r###"
84 Ok(
85 FunctionCall(
86 "foo",
87 [
88 Name(
89 "bar",
90 ),
91 ],
92 ),
93 )
94 "###);
95 insta::assert_debug_snapshot!(parse("foo(bar, baz)"), @r###"
96 Ok(
97 FunctionCall(
98 "foo",
99 [
100 Name(
101 "bar",
102 ),
103 Name(
104 "baz",
105 ),
106 ],
107 ),
108 )
109 "###);
110 insta::assert_debug_snapshot!(parse("foo(bar, baz,)"), @r###"
111 Ok(
112 FunctionCall(
113 "foo",
114 [
115 Name(
116 "bar",
117 ),
118 Name(
119 "baz",
120 ),
121 ],
122 ),
123 )
124 "###);
125 insta::assert_debug_snapshot!(parse("foo(,)"), @r###"
126 Err(
127 ParseError(
128 "Unrecognized token `,` found at 4:5\nExpected one of \"(\", \")\", \"..\", \":\", \"::\", a commit/branch/tag or a string literal",
129 ),
130 )
131 "###);
132 insta::assert_debug_snapshot!(parse("foo(,bar)"), @r###"
133 Err(
134 ParseError(
135 "Unrecognized token `,` found at 4:5\nExpected one of \"(\", \")\", \"..\", \":\", \"::\", a commit/branch/tag or a string literal",
136 ),
137 )
138 "###);
139 insta::assert_debug_snapshot!(parse("foo(bar,,)"), @r###"
140 Err(
141 ParseError(
142 "Unrecognized token `,` found at 8:9\nExpected one of \"(\", \")\", \"..\", \":\", \"::\", a commit/branch/tag or a string literal",
143 ),
144 )
145 "###);
146 Ok(())
147 }
148
149 #[test]
150 fn test_revset_parse_set_operators() -> eyre::Result<()> {
151 insta::assert_debug_snapshot!(parse("foo | bar & bar"), @r###"
152 Ok(
153 FunctionCall(
154 "union",
155 [
156 Name(
157 "foo",
158 ),
159 FunctionCall(
160 "intersection",
161 [
162 Name(
163 "bar",
164 ),
165 Name(
166 "bar",
167 ),
168 ],
169 ),
170 ],
171 ),
172 )
173 "###);
174 insta::assert_debug_snapshot!(parse("foo & bar | bar")?, @r###"
175 FunctionCall(
176 "union",
177 [
178 FunctionCall(
179 "intersection",
180 [
181 Name(
182 "foo",
183 ),
184 Name(
185 "bar",
186 ),
187 ],
188 ),
189 Name(
190 "bar",
191 ),
192 ],
193 )
194 "###);
195 insta::assert_debug_snapshot!(parse("foo | bar")?, @r###"
196 FunctionCall(
197 "union",
198 [
199 Name(
200 "foo",
201 ),
202 Name(
203 "bar",
204 ),
205 ],
206 )
207 "###);
208 insta::assert_debug_snapshot!(parse("foo | bar - baz")?, @r###"
209 FunctionCall(
210 "union",
211 [
212 Name(
213 "foo",
214 ),
215 FunctionCall(
216 "difference",
217 [
218 Name(
219 "bar",
220 ),
221 Name(
222 "baz",
223 ),
224 ],
225 ),
226 ],
227 )
228 "###);
229 insta::assert_debug_snapshot!(parse("foo |"), @r###"
230 Err(
231 ParseError(
232 "Unrecognized EOF found at 5\nExpected one of \"(\", \"..\", \":\", \"::\", a commit/branch/tag or a string literal",
233 ),
234 )
235 "###);
236 Ok(())
237 }
238
239 #[test]
240 fn test_revset_parse_range_operator() -> eyre::Result<()> {
241 insta::assert_debug_snapshot!(parse("foo:bar"), @r###"
242 Ok(
243 FunctionCall(
244 "range",
245 [
246 Name(
247 "foo",
248 ),
249 Name(
250 "bar",
251 ),
252 ],
253 ),
254 )
255 "###);
256 insta::assert_debug_snapshot!(parse("foo:"), @r###"
257 Ok(
258 FunctionCall(
259 "descendants",
260 [
261 Name(
262 "foo",
263 ),
264 ],
265 ),
266 )
267 "###);
268 insta::assert_debug_snapshot!(parse(":foo"), @r###"
269 Ok(
270 FunctionCall(
271 "ancestors",
272 [
273 Name(
274 "foo",
275 ),
276 ],
277 ),
278 )
279 "###);
280
281 insta::assert_debug_snapshot!(parse("foo-bar/baz:qux-grault"), @r###"
282 Ok(
283 FunctionCall(
284 "range",
285 [
286 Name(
287 "foo-bar/baz",
288 ),
289 Name(
290 "qux-grault",
291 ),
292 ],
293 ),
294 )
295 "###);
296
297 insta::assert_debug_snapshot!(parse("foo..bar"), @r###"
298 Ok(
299 FunctionCall(
300 "only",
301 [
302 Name(
303 "bar",
304 ),
305 Name(
306 "foo",
307 ),
308 ],
309 ),
310 )
311 "###);
312 insta::assert_debug_snapshot!(parse("foo.."), @r###"
313 Ok(
314 FunctionCall(
315 "only",
316 [
317 Name(
318 ".",
319 ),
320 Name(
321 "foo",
322 ),
323 ],
324 ),
325 )
326 "###);
327 insta::assert_debug_snapshot!(parse("..bar"), @r###"
328 Ok(
329 FunctionCall(
330 "only",
331 [
332 Name(
333 "bar",
334 ),
335 Name(
336 ".",
337 ),
338 ],
339 ),
340 )
341 "###);
342
343 Ok(())
344 }
345
346 #[test]
347 fn test_revset_parse_string() -> eyre::Result<()> {
348 insta::assert_debug_snapshot!(parse(r#" "" "#), @r###"
349 Ok(
350 Name(
351 "",
352 ),
353 )
354 "###);
355 insta::assert_debug_snapshot!(parse(r#" "foo" "#), @r###"
356 Ok(
357 Name(
358 "foo",
359 ),
360 )
361 "###);
362 insta::assert_debug_snapshot!(parse(r#" "foo bar" "#), @r###"
363 Ok(
364 Name(
365 "foo bar",
366 ),
367 )
368 "###);
369 insta::assert_debug_snapshot!(parse(r#" "foo\nbar\\baz" "#), @r###"
370 Ok(
371 Name(
372 "foo\nba\r\\\\baz",
373 ),
374 )
375 "###);
376 insta::assert_debug_snapshot!(parse(r" 'foo\nbar\\baz' "), @r###"
377 Ok(
378 Name(
379 "foo\nba\r\\\\baz",
380 ),
381 )
382 "###);
383 insta::assert_debug_snapshot!(parse(r#" foo('bar') - baz(qux('qubit')) "#), @r###"
384 Ok(
385 FunctionCall(
386 "difference",
387 [
388 FunctionCall(
389 "foo",
390 [
391 Name(
392 "bar",
393 ),
394 ],
395 ),
396 FunctionCall(
397 "baz",
398 [
399 FunctionCall(
400 "qux",
401 [
402 Name(
403 "qubit",
404 ),
405 ],
406 ),
407 ],
408 ),
409 ],
410 ),
411 )
412 "###);
413
414 Ok(())
415 }
416
417 #[test]
418 fn test_revset_parse_parentheses() -> eyre::Result<()> {
419 insta::assert_debug_snapshot!(parse("((foo()))"), @r###"
420 Ok(
421 FunctionCall(
422 "foo",
423 [],
424 ),
425 )
426 "###);
427 insta::assert_debug_snapshot!(parse("(foo) - bar"), @r###"
428 Ok(
429 FunctionCall(
430 "difference",
431 [
432 Name(
433 "foo",
434 ),
435 Name(
436 "bar",
437 ),
438 ],
439 ),
440 )
441 "###);
442 insta::assert_debug_snapshot!(parse("foo - (bar)"), @r###"
443 Ok(
444 FunctionCall(
445 "difference",
446 [
447 Name(
448 "foo",
449 ),
450 Name(
451 "bar",
452 ),
453 ],
454 ),
455 )
456 "###);
457 insta::assert_debug_snapshot!(parse("(foo) & bar"), @r###"
458 Ok(
459 FunctionCall(
460 "intersection",
461 [
462 Name(
463 "foo",
464 ),
465 Name(
466 "bar",
467 ),
468 ],
469 ),
470 )
471 "###);
472 insta::assert_debug_snapshot!(parse("foo & (bar)"), @r###"
473 Ok(
474 FunctionCall(
475 "intersection",
476 [
477 Name(
478 "foo",
479 ),
480 Name(
481 "bar",
482 ),
483 ],
484 ),
485 )
486 "###);
487 insta::assert_debug_snapshot!(parse("(foo | bar):"), @r###"
488 Ok(
489 FunctionCall(
490 "descendants",
491 [
492 FunctionCall(
493 "union",
494 [
495 Name(
496 "foo",
497 ),
498 Name(
499 "bar",
500 ),
501 ],
502 ),
503 ],
504 ),
505 )
506 "###);
507 insta::assert_debug_snapshot!(parse("(foo)^"), @r###"
508 Ok(
509 FunctionCall(
510 "parents.nth",
511 [
512 Name(
513 "foo",
514 ),
515 Name(
516 "1",
517 ),
518 ],
519 ),
520 )
521 "###);
522
523 Ok(())
524 }
525
526 #[test]
527 fn test_revset_parse_git_revision_syntax() -> eyre::Result<()> {
528 insta::assert_debug_snapshot!(parse("foo:bar^"), @r###"
529 Ok(
530 FunctionCall(
531 "range",
532 [
533 Name(
534 "foo",
535 ),
536 FunctionCall(
537 "parents.nth",
538 [
539 Name(
540 "bar",
541 ),
542 Name(
543 "1",
544 ),
545 ],
546 ),
547 ],
548 ),
549 )
550 "###);
551 insta::assert_debug_snapshot!(parse("foo|bar^"), @r###"
552 Ok(
553 FunctionCall(
554 "union",
555 [
556 Name(
557 "foo",
558 ),
559 FunctionCall(
560 "parents.nth",
561 [
562 Name(
563 "bar",
564 ),
565 Name(
566 "1",
567 ),
568 ],
569 ),
570 ],
571 ),
572 )
573 "###);
574 insta::assert_debug_snapshot!(parse("foo:bar^3"), @r###"
575 Ok(
576 FunctionCall(
577 "range",
578 [
579 Name(
580 "foo",
581 ),
582 FunctionCall(
583 "parents.nth",
584 [
585 Name(
586 "bar",
587 ),
588 Name(
589 "3",
590 ),
591 ],
592 ),
593 ],
594 ),
595 )
596 "###);
597
598 insta::assert_debug_snapshot!(parse("foo:bar~"), @r###"
599 Ok(
600 FunctionCall(
601 "range",
602 [
603 Name(
604 "foo",
605 ),
606 FunctionCall(
607 "ancestors.nth",
608 [
609 Name(
610 "bar",
611 ),
612 Name(
613 "1",
614 ),
615 ],
616 ),
617 ],
618 ),
619 )
620 "###);
621 insta::assert_debug_snapshot!(parse("foo|bar~"), @r###"
622 Ok(
623 FunctionCall(
624 "union",
625 [
626 Name(
627 "foo",
628 ),
629 FunctionCall(
630 "ancestors.nth",
631 [
632 Name(
633 "bar",
634 ),
635 Name(
636 "1",
637 ),
638 ],
639 ),
640 ],
641 ),
642 )
643 "###);
644 insta::assert_debug_snapshot!(parse("foo:bar~3"), @r###"
645 Ok(
646 FunctionCall(
647 "range",
648 [
649 Name(
650 "foo",
651 ),
652 FunctionCall(
653 "ancestors.nth",
654 [
655 Name(
656 "bar",
657 ),
658 Name(
659 "3",
660 ),
661 ],
662 ),
663 ],
664 ),
665 )
666 "###);
667
668 Ok(())
669 }
670}