Skip to main content

bulloak_syntax/
parser.rs

1//! A parser implementation for a stream of tokens representing a bulloak tree.
2use std::{borrow::Borrow, cell::Cell, fmt, result};
3
4use thiserror::Error;
5
6use super::{
7    ast::{Action, Ast, Condition, Description, Root},
8    tokenizer::{Token, TokenKind},
9};
10use crate::{
11    error::FrontendError,
12    span::Span,
13    utils::{repeat_str, sanitize},
14};
15
16type Result<T> = result::Result<T, Error>;
17
18/// An error that occurred while parsing a sequence of tokens into an abstract
19/// syntax tree (AST).
20#[derive(Error, Clone, Debug, Eq, PartialEq)]
21pub struct Error {
22    /// The kind of error.
23    #[source]
24    kind: ErrorKind,
25    /// The original text that the parser generated the error from. Every
26    /// span in an error is a valid range into this string.
27    text: String,
28    /// The span of this error.
29    span: Span,
30}
31
32impl FrontendError<ErrorKind> for Error {
33    /// Return the type of this error.
34    fn kind(&self) -> &ErrorKind {
35        &self.kind
36    }
37
38    /// The original text string in which this error occurred.
39    fn text(&self) -> &str {
40        &self.text
41    }
42
43    /// Return the span at which this error occurred.
44    fn span(&self) -> &Span {
45        &self.span
46    }
47}
48
49impl fmt::Display for Error {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        self.format_error(f)
52    }
53}
54
55type Lexeme = String;
56
57/// The type of an error that occurred while building an AST.
58#[derive(Error, Clone, Debug, Eq, PartialEq)]
59#[non_exhaustive]
60pub enum ErrorKind {
61    /// This might happen because of an internal bug or the user might have
62    /// passed an invalid .tree. An example of how this might be an internal
63    /// bug is if the parser ends up in a state where the current grammar
64    /// production being applied doesn't expect this token to occur.
65    #[error("unexpected token '{0}'")]
66    TokenUnexpected(Lexeme),
67
68    /// Did not expect this token when parsing a description node.
69    #[error("unexpected token in description '{0}'")]
70    DescriptionTokenUnexpected(Lexeme),
71
72    /// Did not expect this When keyword.
73    #[error("unexpected `when` keyword")]
74    WhenUnexpected,
75
76    /// Did not expect this Given keyword.
77    #[error("unexpected `given` keyword")]
78    GivenUnexpected,
79
80    /// Did not expect this It keyword.
81    #[error("unexpected `it` keyword")]
82    ItUnexpected,
83
84    /// Did not expect a Word.
85    #[error("unexpected `word` '{0}'")]
86    WordUnexpected(Lexeme),
87
88    /// Did not expect an end of file.
89    #[error("unexpected end of file")]
90    EofUnexpected,
91
92    /// The token stream was empty, so the tree is empty.
93    #[error("found an empty tree")]
94    TreeEmpty,
95
96    /// A condition or action with no title was found.
97    #[error("found a condition/action without a title")]
98    TitleMissing,
99
100    /// A tree without a root was found.
101    #[error("missing a root")]
102    TreeRootless,
103
104    /// A corner is not the last child.
105    #[error("a `Corner` must be the last child")]
106    CornerNotLastChild,
107
108    /// A tee is the last child.
109    #[error("a `Tee` must not be the last child")]
110    TeeLastChild,
111}
112
113/// A parser for a sequence of .tree tokens into an abstract syntax tree (AST).
114///
115/// This struct represents the state of the parser. It is not
116/// tied to any particular input, while `ParserI` is.
117#[derive(Clone, Default)]
118pub struct Parser {
119    /// The index of the current token.
120    current: Cell<usize>,
121}
122
123impl Parser {
124    /// Create a new parser.
125    #[must_use]
126    pub const fn new() -> Self {
127        Self { current: Cell::new(0) }
128    }
129
130    /// Parse the given tokens into an abstract syntax tree (AST).
131    ///
132    /// `parse` is the entry point for the parser. It takes a sequence of
133    /// tokens and returns an AST.
134    pub fn parse(&mut self, text: &str, tokens: &[Token]) -> Result<Ast> {
135        ParserI::new(self, text, tokens).parse()
136    }
137
138    /// Reset the parser to its initial state.
139    fn reset(&self) {
140        self.current.set(0);
141    }
142}
143
144/// The internal implementation of the parser.
145struct ParserI<'t, P> {
146    /// The input text.
147    text: &'t str,
148    /// The sequence of tokens to parse.
149    tokens: &'t [Token],
150    /// The parser state.
151    parser: P,
152}
153
154impl<'t, P: Borrow<Parser>> ParserI<'t, P> {
155    /// Create a new parser given the parser state, input text, and tokens.
156    const fn new(parser: P, text: &'t str, tokens: &'t [Token]) -> Self {
157        Self { text, tokens, parser }
158    }
159
160    /// Return a reference to the state of the parser.
161    fn parser(&self) -> &Parser {
162        self.parser.borrow()
163    }
164
165    /// Create a new error with the given span and error type.
166    fn error(&self, span: Span, kind: ErrorKind) -> Error {
167        Error { kind, text: self.text.to_owned(), span }
168    }
169
170    /// Returns true if the next call to `current` would
171    /// return `None`.
172    fn is_eof(&self) -> bool {
173        self.parser().current.get() == self.tokens.len()
174    }
175
176    /// Return the current token.
177    ///
178    /// Returns `None` if the parser is past the end
179    /// of the token stream.
180    fn current(&self) -> Option<&Token> {
181        self.tokens.get(self.parser().current.get())
182    }
183
184    /// Return a reference to the next token.
185    ///
186    /// Returns `None` if the parser is currently at, or
187    /// past the end of the token stream.
188    fn peek(&self) -> Option<&Token> {
189        let current_index = self.parser().current.get();
190        self.tokens.get(current_index + 1)
191    }
192
193    /// Return the previous token.
194    ///
195    /// Returns `None` if the parser is currently at the start
196    /// of the token stream.
197    fn previous(&self) -> Option<&Token> {
198        match self.parser().current.get() {
199            0 => None,
200            current => self.tokens.get(current - 1),
201        }
202    }
203
204    /// Move to the next token, returning a reference to it.
205    ///
206    /// If there are no more tokens, return `None`.
207    fn consume(&self) -> Option<&Token> {
208        if self.is_eof() {
209            return None;
210        }
211        self.parser().current.set(self.parser().current.get() + 1);
212        self.tokens.get(self.parser().current.get())
213    }
214
215    /// Parse the given tokens into an abstract syntax tree.
216    ///
217    /// This is the entry point for the parser. Note that
218    /// this method resets the parser state before parsing and
219    /// that we defer the implementation of parsing to `_parse`.
220    pub(crate) fn parse(&self) -> Result<Ast> {
221        self.parser().reset();
222
223        let root_token = self
224            .current()
225            .ok_or(self.error(Span::default(), ErrorKind::TreeEmpty))?;
226
227        match root_token.kind {
228            TokenKind::Word => self.parse_root(root_token),
229            _ => Err(self.error(root_token.span, ErrorKind::TreeRootless)),
230        }
231    }
232
233    /// Parse the root node of the AST.
234    ///
235    /// A root has the form:
236    /// ```grammar
237    /// CONTRACT_NAME
238    /// (<TEE> [Condition | Action])*
239    /// <CORNER> [Condition | Action]
240    /// ```
241    ///
242    /// Panics if called when the parser is not at a `Word` token.
243    fn parse_root(&self, token: &Token) -> Result<Ast> {
244        assert!(matches!(token.kind, TokenKind::Word));
245        self.consume();
246
247        // The loop invariant is that `self.current` is a
248        // `Tee` or the last `Corner`.
249        let mut children = vec![];
250        while let Some(current_token) = self.current() {
251            let child =
252                match current_token.kind {
253                    TokenKind::Corner | TokenKind::Tee => {
254                        self.parse_branch(current_token)?
255                    }
256                    TokenKind::Word => Err(self.error(
257                        current_token.span,
258                        ErrorKind::WordUnexpected(current_token.lexeme.clone()),
259                    ))?,
260                    TokenKind::When => Err(self
261                        .error(current_token.span, ErrorKind::WhenUnexpected))?,
262                    TokenKind::Given => Err(self.error(
263                        current_token.span,
264                        ErrorKind::GivenUnexpected,
265                    ))?,
266                    TokenKind::It => Err(
267                        self.error(current_token.span, ErrorKind::ItUnexpected)
268                    )?,
269                };
270
271            children.push(child);
272        }
273
274        let last_span = if children.is_empty() {
275            &token.span
276        } else {
277            children.iter().last().unwrap().span()
278        };
279
280        Ok(Ast::Root(Root {
281            span: Span::new(token.span.start, last_span.end),
282            children,
283            contract_name: token.lexeme.clone(),
284        }))
285    }
286
287    /// Parse a branch.
288    ///
289    /// A branch is a production that starts with a `Tee` or a `Corner`
290    /// token.
291    ///
292    /// Panics if called when the parser is not at a `Tee` or a `Corner`
293    /// token.
294    fn parse_branch(&self, token: &Token) -> Result<Ast> {
295        assert!(matches!(token.kind, TokenKind::Tee | TokenKind::Corner));
296
297        let first_token = self.peek().ok_or(self.error(
298            token.span.with_start(token.span.end),
299            ErrorKind::EofUnexpected,
300        ))?;
301
302        let ast = match first_token.kind {
303            TokenKind::When | TokenKind::Given => {
304                self.parse_condition(token)?
305            }
306            TokenKind::It => self.parse_action(token)?,
307            _ => Err(self.error(
308                first_token.span,
309                ErrorKind::TokenUnexpected(first_token.lexeme.clone()),
310            ))?,
311        };
312
313        if matches!(token.kind, TokenKind::Tee) && self.is_eof() {
314            return Err(self.error(
315                token.span.with_start(token.span.end),
316                ErrorKind::TeeLastChild,
317            ));
318        } else if matches!(token.kind, TokenKind::Corner) && !self.is_eof() {
319            return Err(self.error(
320                token.span.with_start(token.span.end),
321                ErrorKind::CornerNotLastChild,
322            ));
323        };
324
325        Ok(ast)
326    }
327
328    /// Parse a condition node.
329    ///
330    /// A condition has the form:
331    /// ```grammar
332    /// (<TEE> | <CORNER>) (<WHEN> | <GIVEN>) <WORD>*
333    ///   (<TEE> [Condition | Action])*
334    ///   <CORNER> [Condition | Action]
335    /// ```
336    ///
337    /// Panics if called when the parser is not at a `Tee` or a `Corner`
338    /// token.
339    fn parse_condition(&self, token: &Token) -> Result<Ast> {
340        assert!(matches!(token.kind, TokenKind::Tee | TokenKind::Corner));
341
342        let start_token = self.peek().ok_or(self.error(
343            token.span.with_start(token.span.end),
344            ErrorKind::EofUnexpected,
345        ))?;
346        let title = self.parse_string(start_token);
347
348        if title.len() == start_token.lexeme.len() {
349            return Err(self.error(start_token.span, ErrorKind::TitleMissing));
350        };
351
352        let mut children = vec![];
353        while self
354            .current()
355            // Only parse tokens that are indented more than the current token.
356            // The column determines the tree level we are in.
357            .is_some_and(|t| t.span.start.column > token.span.start.column)
358        {
359            let next_token = self.peek().ok_or(self.error(
360                token.span.with_start(token.span.end),
361                ErrorKind::EofUnexpected,
362            ))?;
363
364            let current_token = self.current().unwrap();
365            let ast = match next_token.kind {
366                TokenKind::When | TokenKind::Given => {
367                    self.parse_condition(current_token)?
368                }
369                TokenKind::It => self.parse_action(current_token)?,
370                _ => Err(self.error(
371                    next_token.span,
372                    ErrorKind::TokenUnexpected(next_token.lexeme.clone()),
373                ))?,
374            };
375
376            children.push(ast);
377        }
378
379        let previous = self.previous().unwrap();
380        Ok(Ast::Condition(Condition {
381            title: sanitize(&title),
382            children,
383            span: Span::new(token.span.start, previous.span.end),
384        }))
385    }
386
387    /// Parse an action node.
388    ///
389    /// An action has the form:
390    /// ```grammar
391    /// (<TEE> | <CORNER>) <IT> <WORD>*
392    ///   (<TEE> ActionDescription)*
393    ///   <CORNER> ActionDescription
394    /// ```
395    ///
396    /// Panics if called when the parser is not at a `Tee` or a `Corner`
397    /// token.
398    fn parse_action(&self, token: &Token) -> Result<Ast> {
399        assert!(matches!(token.kind, TokenKind::Tee | TokenKind::Corner));
400
401        let start_token = self.peek().ok_or(self.error(
402            token.span.with_start(token.span.end),
403            ErrorKind::EofUnexpected,
404        ))?;
405        let title = self.parse_string(start_token);
406
407        let mut children = vec![];
408        while self
409            .current()
410            // Only parse tokens that are indented more than the current token.
411            // The column determines the tree level we are in.
412            .is_some_and(|t| t.span.start.column > token.span.start.column)
413        {
414            let next_token = self.peek().ok_or(self.error(
415                token.span.with_start(token.span.end),
416                ErrorKind::EofUnexpected,
417            ))?;
418
419            let current_token = self.current().unwrap();
420            let ast = match next_token.kind {
421                TokenKind::Word => self.parse_description(
422                    current_token,
423                    current_token.span.start.column - token.span.start.column,
424                )?,
425                _ => Err(self.error(
426                    next_token.span,
427                    ErrorKind::DescriptionTokenUnexpected(
428                        next_token.lexeme.clone(),
429                    ),
430                ))?,
431            };
432
433            children.push(ast);
434        }
435
436        let previous = self.previous().unwrap();
437        Ok(Ast::Action(Action {
438            title,
439            children,
440            span: Span::new(token.span.start, previous.span.end),
441        }))
442    }
443
444    /// Parse an action description node.
445    ///
446    /// An action description has the form:
447    /// ```grammar
448    /// [<TEE> <WORD>* | <CORNER> <WORD>]
449    ///   (<TEE> ActionDescription)*
450    ///   <CORNER> ActionDescription
451    /// ```
452    ///
453    /// This function receives a `column_delta` used to know
454    /// the number of spaces to prepend the lexeme with. E.g.
455    /// For the following action:
456    ///
457    /// ```tree
458    /// It should do something.
459    ///     <CORNER> I describe the above action.
460    /// ^^^^
461    /// ```
462    ///
463    /// Then, `column_delta = 4` and the emitted description should
464    /// respect this.
465    ///
466    /// Panics if called when the parser is not at a `Tee` or a `Corner`
467    /// token.
468    fn parse_description(
469        &self,
470        token: &Token,
471        column_delta: usize,
472    ) -> Result<Ast> {
473        assert!(matches!(token.kind, TokenKind::Tee | TokenKind::Corner));
474
475        let start_token = self.peek().ok_or(self.error(
476            token.span.with_start(token.span.end),
477            ErrorKind::EofUnexpected,
478        ))?;
479        let text = self.parse_string(start_token);
480
481        let previous = self.previous().unwrap();
482        Ok(Ast::ActionDescription(Description {
483            text: format!("{}{}", repeat_str(" ", column_delta), text),
484            span: Span::new(token.span.start, previous.span.end),
485        }))
486    }
487
488    /// Parse a string.
489    ///
490    /// A string is a sequence of words separated by spaces.
491    ///
492    /// Consumes all the tokens including the given token until no more words
493    /// are found.
494    fn parse_string(&self, start_token: &Token) -> String {
495        self.consume();
496        let mut string = String::from(&start_token.lexeme);
497
498        // Consume all words.
499        while let Some(token) = self.consume() {
500            match token.kind {
501                TokenKind::Word
502                | TokenKind::It
503                | TokenKind::When
504                | TokenKind::Given => {
505                    string = string + " " + &token.lexeme;
506                }
507                _ => break,
508            }
509        }
510
511        string
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use indoc::indoc;
518    use pretty_assertions::assert_eq;
519
520    use crate::{
521        ast::{Action, Ast, Condition, Description, Root},
522        parser::{self, ErrorKind, Parser},
523        span::Span,
524        test_utils::{p, s, TestError},
525        tokenizer::Tokenizer,
526    };
527
528    impl PartialEq<parser::Error> for TestError<parser::ErrorKind> {
529        fn eq(&self, other: &parser::Error) -> bool {
530            self.span == other.span && self.kind == other.kind
531        }
532    }
533
534    impl PartialEq<TestError<parser::ErrorKind>> for parser::Error {
535        fn eq(&self, other: &TestError<parser::ErrorKind>) -> bool {
536            self.span == other.span && self.kind == other.kind
537        }
538    }
539
540    fn e<K>(kind: K, span: Span) -> TestError<K> {
541        TestError { kind, span }
542    }
543
544    fn parse(file_contents: &str) -> parser::Result<Ast> {
545        let tokens = Tokenizer::new().tokenize(file_contents).unwrap();
546        Parser::new().parse(file_contents, &tokens)
547    }
548
549    #[test]
550    fn empty_tree() {
551        assert_eq!(
552            parse("").unwrap_err(),
553            e(ErrorKind::TreeEmpty, Span::default())
554        );
555    }
556
557    #[test]
558    fn rootless_tree() {
559        assert_eq!(
560            parse("└── It should never revert.").unwrap_err(),
561            e(ErrorKind::TreeRootless, Span::default())
562        );
563        assert_eq!(
564            parse("├── It should revert.").unwrap_err(),
565            e(ErrorKind::TreeRootless, Span::default())
566        );
567        assert_eq!(
568            parse("└── When stuff happens").unwrap_err(),
569            e(ErrorKind::TreeRootless, Span::default())
570        );
571        assert_eq!(
572            parse("├── When stuff happens").unwrap_err(),
573            e(ErrorKind::TreeRootless, Span::default())
574        );
575        assert_eq!(
576            parse("└── this is a description").unwrap_err(),
577            e(ErrorKind::TreeRootless, Span::default())
578        );
579    }
580
581    #[test]
582    fn tee_last_child_errors() {
583        let input = indoc! {"
584            Foo_Test
585            ├── when something bad happens
586        "};
587
588        assert_eq!(
589            parse(input).unwrap_err(),
590            e(ErrorKind::TeeLastChild, Span::splat(p(9, 2, 1))),
591            "Using a tee (├──) for the last child should result in a TeeLastChild error"
592        );
593    }
594
595    #[test]
596    fn corner_not_last_child_errors() {
597        let input = indoc! {"
598                Foo_Test
599                └── when something bad happens
600                    └── it should revert
601                └── when something happens
602                    └── it should not revert
603        "};
604        assert_eq!(
605            parse(input).unwrap_err(),
606            e(ErrorKind::CornerNotLastChild, Span::splat(p(9, 2, 1)))
607        );
608    }
609
610    #[test]
611    fn only_contract_name() {
612        assert_eq!(
613            parse("FooTest").unwrap(),
614            Ast::Root(Root {
615                span: s(p(0, 1, 1), p(6, 1, 7)),
616                children: vec![],
617                contract_name: String::from("FooTest"),
618            })
619        );
620    }
621
622    #[test]
623    fn one_child() {
624        let input = indoc! {"
625            Foo_Test
626            └── when something bad happens
627               └── it should revert
628        "};
629        assert_eq!(
630            parse(input).unwrap(),
631            Ast::Root(Root {
632                contract_name: String::from("Foo_Test"),
633                span: s(p(0, 1, 1), p(74, 3, 23)),
634                children: vec![Ast::Condition(Condition {
635                    span: s(p(9, 2, 1), p(74, 3, 23)),
636                    title: String::from("when something bad happens"),
637                    children: vec![Ast::Action(Action {
638                        span: s(p(49, 3, 4), p(74, 3, 23)),
639                        title: String::from("it should revert"),
640                        children: vec![]
641                    })],
642                })],
643            })
644        );
645    }
646
647    #[test]
648    fn one_action_description() {
649        let input = indoc! {"
650            Foo_Test
651            └── when something bad happens
652               └── it should revert
653                  └── because _bad_
654        "};
655        assert_eq!(
656            parse(input).unwrap(),
657            Ast::Root(Root {
658                contract_name: String::from("Foo_Test"),
659                span: s(p(0, 1, 1), p(104, 4, 23)),
660                children: vec![Ast::Condition(Condition {
661                    span: s(p(9, 2, 1), p(104, 4, 23)),
662                    title: String::from("when something bad happens"),
663                    children: vec![Ast::Action(Action {
664                        span: s(p(49, 3, 4), p(104, 4, 23)),
665                        title: String::from("it should revert"),
666                        children: vec![Ast::ActionDescription(Description {
667                            span: s(p(82, 4, 7), p(104, 4, 23)),
668                            text: String::from("   because _bad_"),
669                        })]
670                    })],
671                })],
672            })
673        );
674    }
675
676    #[test]
677    fn nested_action_descriptions() {
678        let input = indoc! {"
679            Foo_Test
680            └── when something bad happens
681               └── it should revert
682                  ├── some stuff happened
683                  │  └── and that stuff
684                  └── was very _bad_
685        "};
686        assert_eq!(
687            parse(input).unwrap(),
688            Ast::Root(Root {
689                contract_name: String::from("Foo_Test"),
690                span: s(p(0, 1, 1), p(177, 6, 24)),
691                children: vec![Ast::Condition(Condition {
692                    span: s(p(9, 2, 1), p(177, 6, 24)),
693                    title: String::from("when something bad happens"),
694                    children: vec![Ast::Action(Action {
695                        span: s(p(49, 3, 4), p(177, 6, 24)),
696                        title: String::from("it should revert"),
697                        children: vec![
698                            Ast::ActionDescription(Description {
699                                span: s(p(82, 4, 7), p(110, 4, 29)),
700                                text: String::from("   some stuff happened"),
701                            }),
702                            Ast::ActionDescription(Description {
703                                span: s(p(123, 5, 10), p(146, 5, 27)),
704                                text: String::from("      and that stuff"),
705                            }),
706                            Ast::ActionDescription(Description {
707                                span: s(p(154, 6, 7), p(177, 6, 24)),
708                                text: String::from("   was very _bad_"),
709                            }),
710                        ]
711                    })],
712                })],
713            })
714        );
715    }
716
717    #[test]
718    fn unexpected_tokens() {
719        use ErrorKind::*;
720        assert_eq!(
721            parse(r"a └ └").unwrap_err(),
722            e(TokenUnexpected("└".to_owned()), Span::splat(p(6, 1, 5)))
723        );
724        assert_eq!(
725            parse(r"a ├ ├").unwrap_err(),
726            e(TokenUnexpected("├".to_owned()), Span::splat(p(6, 1, 5)))
727        );
728        assert_eq!(
729            parse(r"a └").unwrap_err(),
730            e(EofUnexpected, Span::splat(p(2, 1, 3)))
731        );
732        assert_eq!(
733            parse(r"a └ when").unwrap_err(),
734            e(TitleMissing, s(p(6, 1, 5), p(9, 1, 8)))
735        );
736        assert_eq!(
737            parse(r"a ├").unwrap_err(),
738            e(EofUnexpected, Span::splat(p(2, 1, 3)))
739        );
740        assert_eq!(
741            parse(r"a when").unwrap_err(),
742            e(WhenUnexpected, s(p(2, 1, 3), p(5, 1, 6)))
743        );
744        assert_eq!(
745            parse(r"a given").unwrap_err(),
746            e(GivenUnexpected, s(p(2, 1, 3), p(6, 1, 7)))
747        );
748        assert_eq!(
749            parse(r"a it").unwrap_err(),
750            e(ItUnexpected, s(p(2, 1, 3), p(3, 1, 4)))
751        );
752        assert_eq!(
753            parse(r"a b").unwrap_err(),
754            e(WordUnexpected("b".to_owned()), Span::splat(p(2, 1, 3)))
755        );
756    }
757
758    #[test]
759    fn descriptions_are_the_only_action_children() {
760        let input = indoc! {"
761            Foo_Test
762            └── when something bad happens
763               └── it should revert
764                  └── it because _bad_
765        "};
766
767        assert_eq!(
768            parse(input).unwrap_err(),
769            e(
770                ErrorKind::DescriptionTokenUnexpected("it".to_owned()),
771                s(p(92, 4, 11), p(93, 4, 12))
772            )
773        );
774    }
775
776    #[test]
777    fn two_children() {
778        let input = indoc! {"
779            FooBarTheBest_Test
780            ├── when stuff called
781            │  └── it should revert
782            └── given not stuff called
783               └── it should revert
784        "};
785
786        assert_eq!(
787            parse(input).unwrap(),
788            Ast::Root(Root {
789                contract_name: String::from("FooBarTheBest_Test"),
790                span: s(p(0, 1, 1), p(140, 5, 23)),
791                children: vec![
792                    Ast::Condition(Condition {
793                        title: String::from("when stuff called"),
794                        span: s(p(19, 2, 1), p(77, 3, 23)),
795                        children: vec![Ast::Action(Action {
796                            title: String::from("it should revert"),
797                            span: s(p(52, 3, 4), p(77, 3, 23)),
798                            children: vec![]
799                        })],
800                    }),
801                    Ast::Condition(Condition {
802                        title: String::from("given not stuff called"),
803                        span: s(p(79, 4, 1), p(140, 5, 23)),
804                        children: vec![Ast::Action(Action {
805                            title: String::from("it should revert"),
806                            span: s(p(115, 5, 4), p(140, 5, 23)),
807                            children: vec![]
808                        })],
809                    }),
810                ],
811            })
812        );
813    }
814
815    // https://github.com/alexfertel/bulloak/issues/54
816    #[test]
817    fn parses_top_level_actions() {
818        let input = indoc! {r#"
819            Foo
820            └── It reverts when X.
821        "#};
822
823        assert_eq!(
824            parse(input).unwrap(),
825            Ast::Root(Root {
826                contract_name: String::from("Foo"),
827                span: s(p(0, 1, 1), p(31, 2, 22)),
828                children: vec![Ast::Action(Action {
829                    title: String::from("It reverts when X."),
830                    span: s(p(4, 2, 1), p(31, 2, 22)),
831                    children: vec![]
832                })],
833            })
834        );
835    }
836
837    #[test]
838    fn unsanitized_input() {
839        let input = indoc! {r#"
840            FooB-rTheBestOf_Test
841            └── when st-ff "all'd
842               └── it should revert
843        "#};
844
845        assert_eq!(
846            parse(input).unwrap(),
847            Ast::Root(Root {
848                contract_name: String::from("FooB-rTheBestOf_Test"),
849                span: s(p(0, 1, 1), p(77, 3, 23)),
850                children: vec![Ast::Condition(Condition {
851                    title: String::from("when st_ff alld"),
852                    span: s(p(21, 2, 1), p(77, 3, 23)),
853                    children: vec![Ast::Action(Action {
854                        title: String::from("it should revert"),
855                        span: s(p(52, 3, 4), p(77, 3, 23)),
856                        children: vec![]
857                    })],
858                })],
859            })
860        );
861    }
862}