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}