pub_just/
parser.rs

1use {super::*, TokenKind::*};
2
3/// Just language parser
4///
5/// The parser is a (hopefully) straightforward recursive descent parser.
6///
7/// It uses a few tokens of lookahead to disambiguate different constructs.
8///
9/// The `expect_*` and `presume_`* methods are similar in that they assert the
10/// type of unparsed tokens and consume them. However, upon encountering an
11/// unexpected token, the `expect_*` methods return an unexpected token error,
12/// whereas the `presume_*` tokens return an internal error.
13///
14/// The `presume_*` methods are used when the token stream has been inspected in
15/// some other way, and thus encountering an unexpected token is a bug in Just,
16/// and not a syntax error.
17///
18/// All methods starting with `parse_*` parse and return a language construct.
19///
20/// The parser tracks an expected set of tokens as it parses. This set contains
21/// all tokens which would have been accepted at the current point in the
22/// parse. Whenever the parser tests for a token that would be accepted, but
23/// does not find it, it adds that token to the set. When the parser accepts a
24/// token, the set is cleared. If the parser finds a token which is unexpected,
25/// the elements of the set are printed in the resultant error message.
26pub struct Parser<'run, 'src> {
27  expected_tokens: BTreeSet<TokenKind>,
28  file_depth: u32,
29  import_offsets: Vec<usize>,
30  module_namepath: &'run Namepath<'src>,
31  next_token: usize,
32  recursion_depth: usize,
33  tokens: &'run [Token<'src>],
34  unstable_features: BTreeSet<UnstableFeature>,
35  working_directory: &'run Path,
36}
37
38impl<'run, 'src> Parser<'run, 'src> {
39  /// Parse `tokens` into an `Ast`
40  pub fn parse(
41    file_depth: u32,
42    import_offsets: &[usize],
43    module_namepath: &'run Namepath<'src>,
44    tokens: &'run [Token<'src>],
45    working_directory: &'run Path,
46  ) -> CompileResult<'src, Ast<'src>> {
47    Self {
48      expected_tokens: BTreeSet::new(),
49      file_depth,
50      import_offsets: import_offsets.to_vec(),
51      module_namepath,
52      next_token: 0,
53      recursion_depth: 0,
54      tokens,
55      unstable_features: BTreeSet::new(),
56      working_directory,
57    }
58    .parse_ast()
59  }
60
61  fn error(&self, kind: CompileErrorKind<'src>) -> CompileResult<'src, CompileError<'src>> {
62    Ok(self.next()?.error(kind))
63  }
64
65  /// Construct an unexpected token error with the token returned by
66  /// `Parser::next`
67  fn unexpected_token(&self) -> CompileResult<'src, CompileError<'src>> {
68    self.error(CompileErrorKind::UnexpectedToken {
69      expected: self
70        .expected_tokens
71        .iter()
72        .copied()
73        .filter(|kind| *kind != ByteOrderMark)
74        .collect::<Vec<TokenKind>>(),
75      found: self.next()?.kind,
76    })
77  }
78
79  fn internal_error(&self, message: impl Into<String>) -> CompileResult<'src, CompileError<'src>> {
80    self.error(CompileErrorKind::Internal {
81      message: message.into(),
82    })
83  }
84
85  /// An iterator over the remaining significant tokens
86  fn rest(&self) -> impl Iterator<Item = Token<'src>> + 'run {
87    self.tokens[self.next_token..]
88      .iter()
89      .copied()
90      .filter(|token| token.kind != Whitespace)
91  }
92
93  /// The next significant token
94  fn next(&self) -> CompileResult<'src, Token<'src>> {
95    if let Some(token) = self.rest().next() {
96      Ok(token)
97    } else {
98      Err(self.internal_error("`Parser::next()` called after end of token stream")?)
99    }
100  }
101
102  /// Check if the next significant token is of kind `kind`
103  fn next_is(&mut self, kind: TokenKind) -> bool {
104    self.next_are(&[kind])
105  }
106
107  /// Check if the next significant tokens are of kinds `kinds`
108  ///
109  /// The first token in `kinds` will be added to the expected token set.
110  fn next_are(&mut self, kinds: &[TokenKind]) -> bool {
111    if let Some(&kind) = kinds.first() {
112      self.expected_tokens.insert(kind);
113    }
114
115    let mut rest = self.rest();
116    for kind in kinds {
117      match rest.next() {
118        Some(token) => {
119          if token.kind != *kind {
120            return false;
121          }
122        }
123        None => return false,
124      }
125    }
126    true
127  }
128
129  /// Advance past one significant token, clearing the expected token set.
130  fn advance(&mut self) -> CompileResult<'src, Token<'src>> {
131    self.expected_tokens.clear();
132
133    for skipped in &self.tokens[self.next_token..] {
134      self.next_token += 1;
135
136      if skipped.kind != Whitespace {
137        return Ok(*skipped);
138      }
139    }
140
141    Err(self.internal_error("`Parser::advance()` advanced past end of token stream")?)
142  }
143
144  /// Return the next token if it is of kind `expected`, otherwise, return an
145  /// unexpected token error
146  fn expect(&mut self, expected: TokenKind) -> CompileResult<'src, Token<'src>> {
147    if let Some(token) = self.accept(expected)? {
148      Ok(token)
149    } else {
150      Err(self.unexpected_token()?)
151    }
152  }
153
154  /// Return an unexpected token error if the next token is not an EOL
155  fn expect_eol(&mut self) -> CompileResult<'src> {
156    self.accept(Comment)?;
157
158    if self.next_is(Eof) {
159      return Ok(());
160    }
161
162    self.expect(Eol).map(|_| ())
163  }
164
165  fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src> {
166    let found = self.advance()?;
167
168    if found.kind == Identifier && expected == found.lexeme() {
169      Ok(())
170    } else {
171      Err(found.error(CompileErrorKind::ExpectedKeyword {
172        expected: vec![expected],
173        found,
174      }))
175    }
176  }
177
178  /// Return an internal error if the next token is not of kind `Identifier`
179  /// with lexeme `lexeme`.
180  fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src> {
181    let next = self.advance()?;
182
183    if next.kind != Identifier {
184      Err(self.internal_error(format!(
185        "Presumed next token would have kind {Identifier}, but found {}",
186        next.kind
187      ))?)
188    } else if keyword == next.lexeme() {
189      Ok(())
190    } else {
191      Err(self.internal_error(format!(
192        "Presumed next token would have lexeme \"{keyword}\", but found \"{}\"",
193        next.lexeme(),
194      ))?)
195    }
196  }
197
198  /// Return an internal error if the next token is not of kind `kind`.
199  fn presume(&mut self, kind: TokenKind) -> CompileResult<'src, Token<'src>> {
200    let next = self.advance()?;
201
202    if next.kind == kind {
203      Ok(next)
204    } else {
205      Err(self.internal_error(format!(
206        "Presumed next token would have kind {kind:?}, but found {:?}",
207        next.kind
208      ))?)
209    }
210  }
211
212  /// Return an internal error if the next token is not one of kinds `kinds`.
213  fn presume_any(&mut self, kinds: &[TokenKind]) -> CompileResult<'src, Token<'src>> {
214    let next = self.advance()?;
215    if kinds.contains(&next.kind) {
216      Ok(next)
217    } else {
218      Err(self.internal_error(format!(
219        "Presumed next token would be {}, but found {}",
220        List::or(kinds),
221        next.kind
222      ))?)
223    }
224  }
225
226  /// Accept and return a token of kind `kind`
227  fn accept(&mut self, kind: TokenKind) -> CompileResult<'src, Option<Token<'src>>> {
228    if self.next_is(kind) {
229      Ok(Some(self.advance()?))
230    } else {
231      Ok(None)
232    }
233  }
234
235  /// Return an error if the next token is of kind `forbidden`
236  fn forbid<F>(&self, forbidden: TokenKind, error: F) -> CompileResult<'src>
237  where
238    F: FnOnce(Token) -> CompileError,
239  {
240    let next = self.next()?;
241
242    if next.kind == forbidden {
243      Err(error(next))
244    } else {
245      Ok(())
246    }
247  }
248
249  /// Accept a token of kind `Identifier` and parse into a `Name`
250  fn accept_name(&mut self) -> CompileResult<'src, Option<Name<'src>>> {
251    if self.next_is(Identifier) {
252      Ok(Some(self.parse_name()?))
253    } else {
254      Ok(None)
255    }
256  }
257
258  fn accepted_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, bool> {
259    let next = self.next()?;
260
261    if next.kind == Identifier && next.lexeme() == keyword.lexeme() {
262      self.advance()?;
263      Ok(true)
264    } else {
265      Ok(false)
266    }
267  }
268
269  /// Accept a dependency
270  fn accept_dependency(&mut self) -> CompileResult<'src, Option<UnresolvedDependency<'src>>> {
271    if let Some(recipe) = self.accept_name()? {
272      Ok(Some(UnresolvedDependency {
273        arguments: Vec::new(),
274        recipe,
275      }))
276    } else if self.accepted(ParenL)? {
277      let recipe = self.parse_name()?;
278
279      let mut arguments = Vec::new();
280
281      while !self.accepted(ParenR)? {
282        arguments.push(self.parse_expression()?);
283      }
284
285      Ok(Some(UnresolvedDependency { recipe, arguments }))
286    } else {
287      Ok(None)
288    }
289  }
290
291  /// Accept and return `true` if next token is of kind `kind`
292  fn accepted(&mut self, kind: TokenKind) -> CompileResult<'src, bool> {
293    Ok(self.accept(kind)?.is_some())
294  }
295
296  /// Parse a justfile, consumes self
297  fn parse_ast(mut self) -> CompileResult<'src, Ast<'src>> {
298    fn pop_doc_comment<'src>(
299      items: &mut Vec<Item<'src>>,
300      eol_since_last_comment: bool,
301    ) -> Option<&'src str> {
302      if !eol_since_last_comment {
303        if let Some(Item::Comment(contents)) = items.last() {
304          let doc = Some(contents[1..].trim_start());
305          items.pop();
306          return doc;
307        }
308      }
309
310      None
311    }
312
313    let mut items = Vec::new();
314
315    let mut eol_since_last_comment = false;
316
317    self.accept(ByteOrderMark)?;
318
319    loop {
320      let mut attributes = self.parse_attributes()?;
321      let mut take_attributes = || {
322        attributes
323          .take()
324          .map(|(_token, attributes)| attributes)
325          .unwrap_or_default()
326      };
327
328      let next = self.next()?;
329
330      if let Some(comment) = self.accept(Comment)? {
331        items.push(Item::Comment(comment.lexeme().trim_end()));
332        self.expect_eol()?;
333        eol_since_last_comment = false;
334      } else if self.accepted(Eol)? {
335        eol_since_last_comment = true;
336      } else if self.accepted(Eof)? {
337        break;
338      } else if self.next_is(Identifier) {
339        match Keyword::from_lexeme(next.lexeme()) {
340          Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
341            items.push(Item::Alias(self.parse_alias(take_attributes())?));
342          }
343          Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
344            self.presume_keyword(Keyword::Export)?;
345            items.push(Item::Assignment(
346              self.parse_assignment(true, take_attributes())?,
347            ));
348          }
349          Some(Keyword::Unexport)
350            if self.next_are(&[Identifier, Identifier, Eof])
351              || self.next_are(&[Identifier, Identifier, Eol]) =>
352          {
353            self.presume_keyword(Keyword::Unexport)?;
354            let name = self.parse_name()?;
355            self.expect_eol()?;
356            items.push(Item::Unexport { name });
357          }
358          Some(Keyword::Import)
359            if self.next_are(&[Identifier, StringToken])
360              || self.next_are(&[Identifier, Identifier, StringToken])
361              || self.next_are(&[Identifier, QuestionMark]) =>
362          {
363            self.presume_keyword(Keyword::Import)?;
364            let optional = self.accepted(QuestionMark)?;
365            let (path, relative) = self.parse_string_literal_token()?;
366            items.push(Item::Import {
367              absolute: None,
368              optional,
369              path,
370              relative,
371            });
372          }
373          Some(Keyword::Mod)
374            if self.next_are(&[Identifier, Identifier, Comment])
375              || self.next_are(&[Identifier, Identifier, Eof])
376              || self.next_are(&[Identifier, Identifier, Eol])
377              || self.next_are(&[Identifier, Identifier, Identifier, StringToken])
378              || self.next_are(&[Identifier, Identifier, StringToken])
379              || self.next_are(&[Identifier, QuestionMark]) =>
380          {
381            let doc = pop_doc_comment(&mut items, eol_since_last_comment);
382
383            self.presume_keyword(Keyword::Mod)?;
384
385            let optional = self.accepted(QuestionMark)?;
386
387            let name = self.parse_name()?;
388
389            let relative = if self.next_is(StringToken) || self.next_are(&[Identifier, StringToken])
390            {
391              Some(self.parse_string_literal()?)
392            } else {
393              None
394            };
395
396            items.push(Item::Module {
397              attributes: take_attributes(),
398              absolute: None,
399              doc,
400              name,
401              optional,
402              relative,
403            });
404          }
405          Some(Keyword::Set)
406            if self.next_are(&[Identifier, Identifier, ColonEquals])
407              || self.next_are(&[Identifier, Identifier, Comment, Eof])
408              || self.next_are(&[Identifier, Identifier, Comment, Eol])
409              || self.next_are(&[Identifier, Identifier, Eof])
410              || self.next_are(&[Identifier, Identifier, Eol]) =>
411          {
412            items.push(Item::Set(self.parse_set()?));
413          }
414          _ => {
415            if self.next_are(&[Identifier, ColonEquals]) {
416              items.push(Item::Assignment(
417                self.parse_assignment(false, take_attributes())?,
418              ));
419            } else {
420              let doc = pop_doc_comment(&mut items, eol_since_last_comment);
421              items.push(Item::Recipe(self.parse_recipe(
422                doc,
423                false,
424                take_attributes(),
425              )?));
426            }
427          }
428        }
429      } else if self.accepted(At)? {
430        let doc = pop_doc_comment(&mut items, eol_since_last_comment);
431        items.push(Item::Recipe(self.parse_recipe(
432          doc,
433          true,
434          take_attributes(),
435        )?));
436      } else {
437        return Err(self.unexpected_token()?);
438      }
439
440      if let Some((token, attributes)) = attributes {
441        return Err(token.error(CompileErrorKind::ExtraneousAttributes {
442          count: attributes.len(),
443        }));
444      }
445    }
446
447    if self.next_token != self.tokens.len() {
448      return Err(self.internal_error(format!(
449        "Parse completed with {} unparsed tokens",
450        self.tokens.len() - self.next_token,
451      ))?);
452    }
453
454    Ok(Ast {
455      items,
456      unstable_features: self.unstable_features,
457      warnings: Vec::new(),
458      working_directory: self.working_directory.into(),
459    })
460  }
461
462  /// Parse an alias, e.g `alias name := target`
463  fn parse_alias(
464    &mut self,
465    attributes: BTreeSet<Attribute<'src>>,
466  ) -> CompileResult<'src, Alias<'src, Name<'src>>> {
467    self.presume_keyword(Keyword::Alias)?;
468    let name = self.parse_name()?;
469    self.presume_any(&[Equals, ColonEquals])?;
470    let target = self.parse_name()?;
471    self.expect_eol()?;
472    Ok(Alias {
473      attributes,
474      name,
475      target,
476    })
477  }
478
479  /// Parse an assignment, e.g. `foo := bar`
480  fn parse_assignment(
481    &mut self,
482    export: bool,
483    attributes: BTreeSet<Attribute<'src>>,
484  ) -> CompileResult<'src, Assignment<'src>> {
485    let name = self.parse_name()?;
486    self.presume(ColonEquals)?;
487    let value = self.parse_expression()?;
488    self.expect_eol()?;
489
490    let private = attributes.contains(&Attribute::Private);
491
492    for attribute in attributes {
493      if attribute != Attribute::Private {
494        return Err(name.error(CompileErrorKind::InvalidAttribute {
495          item_kind: "Assignment",
496          item_name: name.lexeme(),
497          attribute,
498        }));
499      }
500    }
501
502    Ok(Assignment {
503      constant: false,
504      export,
505      file_depth: self.file_depth,
506      name,
507      private: private || name.lexeme().starts_with('_'),
508      value,
509    })
510  }
511
512  /// Parse an expression, e.g. `1 + 2`
513  fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> {
514    if self.recursion_depth == if cfg!(windows) { 48 } else { 256 } {
515      let token = self.next()?;
516      return Err(CompileError::new(
517        token,
518        CompileErrorKind::ParsingRecursionDepthExceeded,
519      ));
520    }
521
522    self.recursion_depth += 1;
523
524    let disjunct = self.parse_disjunct()?;
525
526    let expression = if self.accepted(BarBar)? {
527      self
528        .unstable_features
529        .insert(UnstableFeature::LogicalOperators);
530      let lhs = disjunct.into();
531      let rhs = self.parse_expression()?.into();
532      Expression::Or { lhs, rhs }
533    } else {
534      disjunct
535    };
536
537    self.recursion_depth -= 1;
538
539    Ok(expression)
540  }
541
542  fn parse_disjunct(&mut self) -> CompileResult<'src, Expression<'src>> {
543    let conjunct = self.parse_conjunct()?;
544
545    let disjunct = if self.accepted(AmpersandAmpersand)? {
546      self
547        .unstable_features
548        .insert(UnstableFeature::LogicalOperators);
549      let lhs = conjunct.into();
550      let rhs = self.parse_disjunct()?.into();
551      Expression::And { lhs, rhs }
552    } else {
553      conjunct
554    };
555
556    Ok(disjunct)
557  }
558
559  fn parse_conjunct(&mut self) -> CompileResult<'src, Expression<'src>> {
560    if self.accepted_keyword(Keyword::If)? {
561      self.parse_conditional()
562    } else if self.accepted(Slash)? {
563      let lhs = None;
564      let rhs = self.parse_conjunct()?.into();
565      Ok(Expression::Join { lhs, rhs })
566    } else {
567      let value = self.parse_value()?;
568
569      if self.accepted(Slash)? {
570        let lhs = Some(Box::new(value));
571        let rhs = self.parse_conjunct()?.into();
572        Ok(Expression::Join { lhs, rhs })
573      } else if self.accepted(Plus)? {
574        let lhs = value.into();
575        let rhs = self.parse_conjunct()?.into();
576        Ok(Expression::Concatenation { lhs, rhs })
577      } else {
578        Ok(value)
579      }
580    }
581  }
582
583  /// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
584  fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
585    let condition = self.parse_condition()?;
586
587    self.expect(BraceL)?;
588
589    let then = self.parse_expression()?;
590
591    self.expect(BraceR)?;
592
593    self.expect_keyword(Keyword::Else)?;
594
595    let otherwise = if self.accepted_keyword(Keyword::If)? {
596      self.parse_conditional()?
597    } else {
598      self.expect(BraceL)?;
599      let otherwise = self.parse_expression()?;
600      self.expect(BraceR)?;
601      otherwise
602    };
603
604    Ok(Expression::Conditional {
605      condition,
606      then: then.into(),
607      otherwise: otherwise.into(),
608    })
609  }
610
611  fn parse_condition(&mut self) -> CompileResult<'src, Condition<'src>> {
612    let lhs = self.parse_expression()?;
613    let operator = if self.accepted(BangEquals)? {
614      ConditionalOperator::Inequality
615    } else if self.accepted(EqualsTilde)? {
616      ConditionalOperator::RegexMatch
617    } else {
618      self.expect(EqualsEquals)?;
619      ConditionalOperator::Equality
620    };
621    let rhs = self.parse_expression()?;
622    Ok(Condition {
623      lhs: lhs.into(),
624      rhs: rhs.into(),
625      operator,
626    })
627  }
628
629  // Check if the next tokens are a shell-expanded string, i.e., `x"foo"`.
630  //
631  // This function skips initial whitespace tokens, but thereafter is
632  // whitespace-sensitive, so `x"foo"` is a shell-expanded string, whereas `x
633  // "foo"` is not.
634  fn next_is_shell_expanded_string(&self) -> bool {
635    let mut tokens = self
636      .tokens
637      .iter()
638      .skip(self.next_token)
639      .skip_while(|token| token.kind == Whitespace);
640
641    tokens
642      .next()
643      .is_some_and(|token| token.kind == Identifier && token.lexeme() == "x")
644      && tokens.next().is_some_and(|token| token.kind == StringToken)
645  }
646
647  /// Parse a value, e.g. `(bar)`
648  fn parse_value(&mut self) -> CompileResult<'src, Expression<'src>> {
649    if self.next_is(StringToken) || self.next_is_shell_expanded_string() {
650      Ok(Expression::StringLiteral {
651        string_literal: self.parse_string_literal()?,
652      })
653    } else if self.next_is(Backtick) {
654      let next = self.next()?;
655      let kind = StringKind::from_string_or_backtick(next)?;
656      let contents =
657        &next.lexeme()[kind.delimiter_len()..next.lexeme().len() - kind.delimiter_len()];
658      let token = self.advance()?;
659      let contents = if kind.indented() {
660        unindent(contents)
661      } else {
662        contents.to_owned()
663      };
664
665      if contents.starts_with("#!") {
666        return Err(next.error(CompileErrorKind::BacktickShebang));
667      }
668      Ok(Expression::Backtick { contents, token })
669    } else if self.next_is(Identifier) {
670      if self.accepted_keyword(Keyword::Assert)? {
671        self.expect(ParenL)?;
672        let condition = self.parse_condition()?;
673        self.expect(Comma)?;
674        let error = Box::new(self.parse_expression()?);
675        self.expect(ParenR)?;
676        Ok(Expression::Assert { condition, error })
677      } else {
678        let name = self.parse_name()?;
679
680        if self.next_is(ParenL) {
681          let arguments = self.parse_sequence()?;
682          Ok(Expression::Call {
683            thunk: Thunk::resolve(name, arguments)?,
684          })
685        } else {
686          Ok(Expression::Variable { name })
687        }
688      }
689    } else if self.next_is(ParenL) {
690      self.presume(ParenL)?;
691      let contents = self.parse_expression()?.into();
692      self.expect(ParenR)?;
693      Ok(Expression::Group { contents })
694    } else {
695      Err(self.unexpected_token()?)
696    }
697  }
698
699  /// Parse a string literal, e.g. `"FOO"`, returning the string literal and the string token
700  fn parse_string_literal_token(
701    &mut self,
702  ) -> CompileResult<'src, (Token<'src>, StringLiteral<'src>)> {
703    let expand = if self.next_is(Identifier) {
704      self.expect_keyword(Keyword::X)?;
705      true
706    } else {
707      false
708    };
709
710    let token = self.expect(StringToken)?;
711
712    let kind = StringKind::from_string_or_backtick(token)?;
713
714    let delimiter_len = kind.delimiter_len();
715
716    let raw = &token.lexeme()[delimiter_len..token.lexeme().len() - delimiter_len];
717
718    let unindented = if kind.indented() {
719      unindent(raw)
720    } else {
721      raw.to_owned()
722    };
723
724    let cooked = if kind.processes_escape_sequences() {
725      Self::cook_string(token, &unindented)?
726    } else {
727      unindented
728    };
729
730    let cooked = if expand {
731      shellexpand::full(&cooked)
732        .map_err(|err| token.error(CompileErrorKind::ShellExpansion { err }))?
733        .into_owned()
734    } else {
735      cooked
736    };
737
738    Ok((
739      token,
740      StringLiteral {
741        cooked,
742        expand,
743        kind,
744        raw,
745      },
746    ))
747  }
748
749  // Transform escape sequences in from string literal `token` with content `text`
750  fn cook_string(token: Token<'src>, text: &str) -> CompileResult<'src, String> {
751    #[derive(PartialEq, Eq)]
752    enum State {
753      Initial,
754      Backslash,
755      Unicode,
756      UnicodeValue { hex: String },
757    }
758
759    let mut cooked = String::new();
760
761    let mut state = State::Initial;
762
763    for c in text.chars() {
764      match state {
765        State::Initial => {
766          if c == '\\' {
767            state = State::Backslash;
768          } else {
769            cooked.push(c);
770          }
771        }
772        State::Backslash if c == 'u' => {
773          state = State::Unicode;
774        }
775        State::Backslash => {
776          match c {
777            'n' => cooked.push('\n'),
778            'r' => cooked.push('\r'),
779            't' => cooked.push('\t'),
780            '\\' => cooked.push('\\'),
781            '\n' => {}
782            '"' => cooked.push('"'),
783            character => {
784              return Err(token.error(CompileErrorKind::InvalidEscapeSequence { character }))
785            }
786          }
787          state = State::Initial;
788        }
789        State::Unicode => match c {
790          '{' => {
791            state = State::UnicodeValue { hex: String::new() };
792          }
793          character => {
794            return Err(token.error(CompileErrorKind::UnicodeEscapeDelimiter { character }));
795          }
796        },
797        State::UnicodeValue { ref mut hex } => match c {
798          '}' => {
799            if hex.is_empty() {
800              return Err(token.error(CompileErrorKind::UnicodeEscapeEmpty));
801            }
802
803            let codepoint = u32::from_str_radix(hex, 16).unwrap();
804
805            cooked.push(char::from_u32(codepoint).ok_or_else(|| {
806              token.error(CompileErrorKind::UnicodeEscapeRange { hex: hex.clone() })
807            })?);
808
809            state = State::Initial;
810          }
811          '0'..='9' | 'A'..='F' | 'a'..='f' => {
812            hex.push(c);
813            if hex.len() > 6 {
814              return Err(token.error(CompileErrorKind::UnicodeEscapeLength { hex: hex.clone() }));
815            }
816          }
817          _ => {
818            return Err(token.error(CompileErrorKind::UnicodeEscapeCharacter { character: c }));
819          }
820        },
821      }
822    }
823
824    if state != State::Initial {
825      return Err(token.error(CompileErrorKind::UnicodeEscapeUnterminated));
826    }
827
828    Ok(cooked)
829  }
830
831  /// Parse a string literal, e.g. `"FOO"`
832  fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> {
833    let (_token, string_literal) = self.parse_string_literal_token()?;
834    Ok(string_literal)
835  }
836
837  /// Parse a name from an identifier token
838  fn parse_name(&mut self) -> CompileResult<'src, Name<'src>> {
839    self.expect(Identifier).map(Name::from_identifier)
840  }
841
842  /// Parse sequence of comma-separated expressions
843  fn parse_sequence(&mut self) -> CompileResult<'src, Vec<Expression<'src>>> {
844    self.presume(ParenL)?;
845
846    let mut elements = Vec::new();
847
848    while !self.next_is(ParenR) {
849      elements.push(self.parse_expression()?);
850
851      if !self.accepted(Comma)? {
852        break;
853      }
854    }
855
856    self.expect(ParenR)?;
857
858    Ok(elements)
859  }
860
861  /// Parse a recipe
862  fn parse_recipe(
863    &mut self,
864    doc: Option<&'src str>,
865    quiet: bool,
866    attributes: BTreeSet<Attribute<'src>>,
867  ) -> CompileResult<'src, UnresolvedRecipe<'src>> {
868    let name = self.parse_name()?;
869
870    let mut positional = Vec::new();
871
872    while self.next_is(Identifier) || self.next_is(Dollar) {
873      positional.push(self.parse_parameter(ParameterKind::Singular)?);
874    }
875
876    let kind = if self.accepted(Plus)? {
877      ParameterKind::Plus
878    } else if self.accepted(Asterisk)? {
879      ParameterKind::Star
880    } else {
881      ParameterKind::Singular
882    };
883
884    let variadic = if kind.is_variadic() {
885      let variadic = self.parse_parameter(kind)?;
886
887      self.forbid(Identifier, |token| {
888        token.error(CompileErrorKind::ParameterFollowsVariadicParameter {
889          parameter: token.lexeme(),
890        })
891      })?;
892
893      Some(variadic)
894    } else {
895      None
896    };
897
898    self.expect(Colon)?;
899
900    let mut dependencies = Vec::new();
901
902    while let Some(dependency) = self.accept_dependency()? {
903      dependencies.push(dependency);
904    }
905
906    let priors = dependencies.len();
907
908    if self.accepted(AmpersandAmpersand)? {
909      let mut subsequents = Vec::new();
910
911      while let Some(subsequent) = self.accept_dependency()? {
912        subsequents.push(subsequent);
913      }
914
915      if subsequents.is_empty() {
916        return Err(self.unexpected_token()?);
917      }
918
919      dependencies.append(&mut subsequents);
920    }
921
922    self.expect_eol()?;
923
924    let body = self.parse_body()?;
925
926    let shebang = body.first().map_or(false, Line::is_shebang);
927    let script = attributes
928      .iter()
929      .any(|attribute| matches!(attribute, Attribute::Script(_)));
930
931    if shebang && script {
932      return Err(name.error(CompileErrorKind::ShebangAndScriptAttribute {
933        recipe: name.lexeme(),
934      }));
935    }
936
937    let private = name.lexeme().starts_with('_') || attributes.contains(&Attribute::Private);
938
939    let mut doc = doc.map(ToOwned::to_owned);
940
941    for attribute in &attributes {
942      if let Attribute::Doc(attribute_doc) = attribute {
943        doc = attribute_doc.as_ref().map(|doc| doc.cooked.clone());
944      }
945    }
946
947    Ok(Recipe {
948      shebang: shebang || script,
949      attributes,
950      body,
951      dependencies,
952      doc,
953      file_depth: self.file_depth,
954      import_offsets: self.import_offsets.clone(),
955      name,
956      namepath: self.module_namepath.join(name),
957      parameters: positional.into_iter().chain(variadic).collect(),
958      priors,
959      private,
960      quiet,
961    })
962  }
963
964  /// Parse a recipe parameter
965  fn parse_parameter(&mut self, kind: ParameterKind) -> CompileResult<'src, Parameter<'src>> {
966    let export = self.accepted(Dollar)?;
967
968    let name = self.parse_name()?;
969
970    let default = if self.accepted(Equals)? {
971      Some(self.parse_value()?)
972    } else {
973      None
974    };
975
976    Ok(Parameter {
977      default,
978      export,
979      kind,
980      name,
981    })
982  }
983
984  /// Parse the body of a recipe
985  fn parse_body(&mut self) -> CompileResult<'src, Vec<Line<'src>>> {
986    let mut lines = Vec::new();
987
988    if self.accepted(Indent)? {
989      while !self.accepted(Dedent)? {
990        let mut fragments = Vec::new();
991        let number = self
992          .tokens
993          .get(self.next_token)
994          .map(|token| token.line)
995          .unwrap_or_default();
996
997        if !self.accepted(Eol)? {
998          while !(self.accepted(Eol)? || self.next_is(Dedent)) {
999            if let Some(token) = self.accept(Text)? {
1000              fragments.push(Fragment::Text { token });
1001            } else if self.accepted(InterpolationStart)? {
1002              fragments.push(Fragment::Interpolation {
1003                expression: self.parse_expression()?,
1004              });
1005              self.expect(InterpolationEnd)?;
1006            } else {
1007              return Err(self.unexpected_token()?);
1008            }
1009          }
1010        };
1011
1012        lines.push(Line { fragments, number });
1013      }
1014    }
1015
1016    while lines.last().map_or(false, Line::is_empty) {
1017      lines.pop();
1018    }
1019
1020    Ok(lines)
1021  }
1022
1023  /// Parse a boolean setting value
1024  fn parse_set_bool(&mut self) -> CompileResult<'src, bool> {
1025    if !self.accepted(ColonEquals)? {
1026      return Ok(true);
1027    }
1028
1029    let identifier = self.expect(Identifier)?;
1030
1031    let value = if Keyword::True == identifier.lexeme() {
1032      true
1033    } else if Keyword::False == identifier.lexeme() {
1034      false
1035    } else {
1036      return Err(identifier.error(CompileErrorKind::ExpectedKeyword {
1037        expected: vec![Keyword::True, Keyword::False],
1038        found: identifier,
1039      }));
1040    };
1041
1042    Ok(value)
1043  }
1044
1045  /// Parse a setting
1046  fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> {
1047    self.presume_keyword(Keyword::Set)?;
1048    let name = Name::from_identifier(self.presume(Identifier)?);
1049    let lexeme = name.lexeme();
1050    let Some(keyword) = Keyword::from_lexeme(lexeme) else {
1051      return Err(name.error(CompileErrorKind::UnknownSetting {
1052        setting: name.lexeme(),
1053      }));
1054    };
1055
1056    let set_bool = match keyword {
1057      Keyword::AllowDuplicateRecipes => {
1058        Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))
1059      }
1060      Keyword::AllowDuplicateVariables => {
1061        Some(Setting::AllowDuplicateVariables(self.parse_set_bool()?))
1062      }
1063      Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
1064      Keyword::DotenvRequired => Some(Setting::DotenvRequired(self.parse_set_bool()?)),
1065      Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
1066      Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
1067      Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
1068      Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
1069      Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
1070      Keyword::Unstable => Some(Setting::Unstable(self.parse_set_bool()?)),
1071      Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
1072      _ => None,
1073    };
1074
1075    if let Some(value) = set_bool {
1076      return Ok(Set { name, value });
1077    }
1078
1079    self.expect(ColonEquals)?;
1080
1081    let set_value = match keyword {
1082      Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?)),
1083      Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?)),
1084      Keyword::ScriptInterpreter => Some(Setting::ScriptInterpreter(self.parse_interpreter()?)),
1085      Keyword::Shell => Some(Setting::Shell(self.parse_interpreter()?)),
1086      Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?)),
1087      Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_interpreter()?)),
1088      Keyword::WorkingDirectory => Some(Setting::WorkingDirectory(self.parse_string_literal()?)),
1089      _ => None,
1090    };
1091
1092    if let Some(value) = set_value {
1093      return Ok(Set { name, value });
1094    }
1095
1096    Err(name.error(CompileErrorKind::UnknownSetting {
1097      setting: name.lexeme(),
1098    }))
1099  }
1100
1101  /// Parse interpreter setting value, i.e., `['sh', '-eu']`
1102  fn parse_interpreter(&mut self) -> CompileResult<'src, Interpreter<'src>> {
1103    self.expect(BracketL)?;
1104
1105    let command = self.parse_string_literal()?;
1106
1107    let mut arguments = Vec::new();
1108
1109    if self.accepted(Comma)? {
1110      while !self.next_is(BracketR) {
1111        arguments.push(self.parse_string_literal()?);
1112
1113        if !self.accepted(Comma)? {
1114          break;
1115        }
1116      }
1117    }
1118
1119    self.expect(BracketR)?;
1120
1121    Ok(Interpreter { arguments, command })
1122  }
1123
1124  /// Item attributes, i.e., `[macos]` or `[confirm: "warning!"]`
1125  fn parse_attributes(
1126    &mut self,
1127  ) -> CompileResult<'src, Option<(Token<'src>, BTreeSet<Attribute<'src>>)>> {
1128    let mut attributes = BTreeMap::new();
1129
1130    let mut token = None;
1131
1132    while let Some(bracket) = self.accept(BracketL)? {
1133      token.get_or_insert(bracket);
1134
1135      loop {
1136        let name = self.parse_name()?;
1137
1138        let mut arguments = Vec::new();
1139
1140        if self.accepted(Colon)? {
1141          arguments.push(self.parse_string_literal()?);
1142        } else if self.accepted(ParenL)? {
1143          loop {
1144            arguments.push(self.parse_string_literal()?);
1145
1146            if !self.accepted(Comma)? {
1147              break;
1148            }
1149          }
1150          self.expect(ParenR)?;
1151        }
1152
1153        let attribute = Attribute::new(name, arguments)?;
1154
1155        if let Some(line) = attributes.get(&attribute) {
1156          return Err(name.error(CompileErrorKind::DuplicateAttribute {
1157            attribute: name.lexeme(),
1158            first: *line,
1159          }));
1160        }
1161
1162        attributes.insert(attribute, name.line);
1163
1164        if !self.accepted(Comma)? {
1165          break;
1166        }
1167      }
1168      self.expect(BracketR)?;
1169      self.expect_eol()?;
1170    }
1171
1172    if attributes.is_empty() {
1173      Ok(None)
1174    } else {
1175      Ok(Some((token.unwrap(), attributes.into_keys().collect())))
1176    }
1177  }
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182  use super::*;
1183
1184  use pretty_assertions::assert_eq;
1185  use CompileErrorKind::*;
1186
1187  macro_rules! test {
1188    {
1189      name: $name:ident,
1190      text: $text:expr,
1191      tree: $tree:tt,
1192    } => {
1193      #[test]
1194      fn $name() {
1195        let text: String = $text.into();
1196        let want = tree!($tree);
1197        test(&text, want);
1198      }
1199    }
1200  }
1201
1202  fn test(text: &str, want: Tree) {
1203    let unindented = unindent(text);
1204    let tokens = Lexer::test_lex(&unindented).expect("lexing failed");
1205    let justfile = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())
1206      .expect("parsing failed");
1207    let have = justfile.tree();
1208    if have != want {
1209      println!("parsed text: {unindented}");
1210      println!("expected:    {want}");
1211      println!("but got:     {have}");
1212      println!("tokens:      {tokens:?}");
1213      panic!();
1214    }
1215  }
1216
1217  macro_rules! error {
1218    (
1219      name:   $name:ident,
1220      input:  $input:expr,
1221      offset: $offset:expr,
1222      line:   $line:expr,
1223      column: $column:expr,
1224      width:  $width:expr,
1225      kind:   $kind:expr,
1226    ) => {
1227      #[test]
1228      fn $name() {
1229        error($input, $offset, $line, $column, $width, $kind);
1230      }
1231    };
1232  }
1233
1234  fn error(
1235    src: &str,
1236    offset: usize,
1237    line: usize,
1238    column: usize,
1239    length: usize,
1240    kind: CompileErrorKind,
1241  ) {
1242    let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
1243
1244    match Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new()) {
1245      Ok(_) => panic!("Parsing unexpectedly succeeded"),
1246      Err(have) => {
1247        let want = CompileError {
1248          token: Token {
1249            kind: have.token.kind,
1250            src,
1251            offset,
1252            line,
1253            column,
1254            length,
1255            path: "justfile".as_ref(),
1256          },
1257          kind: kind.into(),
1258        };
1259        assert_eq!(have, want);
1260      }
1261    }
1262  }
1263
1264  test! {
1265    name: empty,
1266    text: "",
1267    tree: (justfile),
1268  }
1269
1270  test! {
1271    name: empty_multiline,
1272    text: "
1273
1274
1275
1276
1277
1278    ",
1279    tree: (justfile),
1280  }
1281
1282  test! {
1283    name: whitespace,
1284    text: " ",
1285    tree: (justfile),
1286  }
1287
1288  test! {
1289    name: alias_single,
1290    text: "alias t := test",
1291    tree: (justfile (alias t test)),
1292  }
1293
1294  test! {
1295    name: alias_with_attribute,
1296    text: "[private]\nalias t := test",
1297    tree: (justfile (alias t test)),
1298  }
1299
1300  test! {
1301    name: single_argument_attribute_shorthand,
1302    text: "[group: 'some-group']\nalias t := test",
1303    tree: (justfile (alias t test)),
1304  }
1305
1306  test! {
1307    name: single_argument_attribute_shorthand_multiple_same_line,
1308    text: "[group: 'some-group', group: 'some-other-group']\nalias t := test",
1309    tree: (justfile (alias t test)),
1310  }
1311
1312  test! {
1313    name: aliases_multiple,
1314    text: "alias t := test\nalias b := build",
1315    tree: (
1316      justfile
1317      (alias t test)
1318      (alias b build)
1319    ),
1320  }
1321
1322  test! {
1323    name: alias_equals,
1324    text: "alias t := test",
1325    tree: (justfile
1326      (alias t test)
1327    ),
1328  }
1329
1330  test! {
1331      name: recipe_named_alias,
1332      text: r"
1333      [private]
1334      alias:
1335        echo 'echoing alias'
1336          ",
1337    tree: (justfile
1338      (recipe alias (body ("echo 'echoing alias'")))
1339    ),
1340  }
1341
1342  test! {
1343    name: export,
1344    text: r#"export x := "hello""#,
1345    tree: (justfile (assignment #export x "hello")),
1346  }
1347
1348  test! {
1349    name: private_export,
1350    text: "
1351      [private]
1352      export x := 'hello'
1353    ",
1354    tree: (justfile (assignment #export x "hello")),
1355  }
1356
1357  test! {
1358    name: export_equals,
1359    text: r#"export x := "hello""#,
1360    tree: (justfile
1361      (assignment #export x "hello")
1362    ),
1363  }
1364
1365  test! {
1366    name: assignment,
1367    text: r#"x := "hello""#,
1368    tree: (justfile (assignment x "hello")),
1369  }
1370
1371  test! {
1372    name: private_assignment,
1373    text: "
1374      [private]
1375      x := 'hello'
1376      ",
1377    tree: (justfile (assignment x "hello")),
1378  }
1379
1380  test! {
1381    name: assignment_equals,
1382    text: r#"x := "hello""#,
1383    tree: (justfile
1384      (assignment x "hello")
1385    ),
1386  }
1387
1388  test! {
1389    name: backtick,
1390    text: "x := `hello`",
1391    tree: (justfile (assignment x (backtick "hello"))),
1392  }
1393
1394  test! {
1395    name: variable,
1396    text: "x := y",
1397    tree: (justfile (assignment x y)),
1398  }
1399
1400  test! {
1401    name: group,
1402    text: "x := (y)",
1403    tree: (justfile (assignment x (y))),
1404  }
1405
1406  test! {
1407    name: addition_single,
1408    text: "x := a + b",
1409    tree: (justfile (assignment x (+ a b))),
1410  }
1411
1412  test! {
1413    name: addition_chained,
1414    text: "x := a + b + c",
1415    tree: (justfile (assignment x (+ a (+ b c)))),
1416  }
1417
1418  test! {
1419    name: call_one_arg,
1420    text: "x := env_var(y)",
1421    tree: (justfile (assignment x (call env_var y))),
1422  }
1423
1424  test! {
1425    name: call_multiple_args,
1426    text: "x := env_var_or_default(y, z)",
1427    tree: (justfile (assignment x (call env_var_or_default y z))),
1428  }
1429
1430  test! {
1431    name: call_trailing_comma,
1432    text: "x := env_var(y,)",
1433    tree: (justfile (assignment x (call env_var y))),
1434  }
1435
1436  test! {
1437    name: recipe,
1438    text: "foo:",
1439    tree: (justfile (recipe foo)),
1440  }
1441
1442  test! {
1443    name: recipe_multiple,
1444    text: "
1445      foo:
1446      bar:
1447      baz:
1448    ",
1449    tree: (justfile (recipe foo) (recipe bar) (recipe baz)),
1450  }
1451
1452  test! {
1453    name: recipe_quiet,
1454    text: "@foo:",
1455    tree: (justfile (recipe #quiet foo)),
1456  }
1457
1458  test! {
1459    name: recipe_parameter_single,
1460    text: "foo bar:",
1461    tree: (justfile (recipe foo (params (bar)))),
1462  }
1463
1464  test! {
1465    name: recipe_parameter_multiple,
1466    text: "foo bar baz:",
1467    tree: (justfile (recipe foo (params (bar) (baz)))),
1468  }
1469
1470  test! {
1471    name: recipe_default_single,
1472    text: r#"foo bar="baz":"#,
1473    tree: (justfile (recipe foo (params (bar "baz")))),
1474  }
1475
1476  test! {
1477    name: recipe_default_multiple,
1478    text: r#"foo bar="baz" bob="biz":"#,
1479    tree: (justfile (recipe foo (params (bar "baz") (bob "biz")))),
1480  }
1481
1482  test! {
1483    name: recipe_plus_variadic,
1484    text: r"foo +bar:",
1485    tree: (justfile (recipe foo (params +(bar)))),
1486  }
1487
1488  test! {
1489    name: recipe_star_variadic,
1490    text: r"foo *bar:",
1491    tree: (justfile (recipe foo (params *(bar)))),
1492  }
1493
1494  test! {
1495    name: recipe_variadic_string_default,
1496    text: r#"foo +bar="baz":"#,
1497    tree: (justfile (recipe foo (params +(bar "baz")))),
1498  }
1499
1500  test! {
1501    name: recipe_variadic_variable_default,
1502    text: r"foo +bar=baz:",
1503    tree: (justfile (recipe foo (params +(bar baz)))),
1504  }
1505
1506  test! {
1507    name: recipe_variadic_addition_group_default,
1508    text: r"foo +bar=(baz + bob):",
1509    tree: (justfile (recipe foo (params +(bar ((+ baz bob)))))),
1510  }
1511
1512  test! {
1513    name: recipe_dependency_single,
1514    text: "foo: bar",
1515    tree: (justfile (recipe foo (deps bar))),
1516  }
1517
1518  test! {
1519    name: recipe_dependency_multiple,
1520    text: "foo: bar baz",
1521    tree: (justfile (recipe foo (deps bar baz))),
1522  }
1523
1524  test! {
1525    name: recipe_dependency_parenthesis,
1526    text: "foo: (bar)",
1527    tree: (justfile (recipe foo (deps bar))),
1528  }
1529
1530  test! {
1531    name: recipe_dependency_argument_string,
1532    text: "foo: (bar 'baz')",
1533    tree: (justfile (recipe foo (deps (bar "baz")))),
1534  }
1535
1536  test! {
1537    name: recipe_dependency_argument_identifier,
1538    text: "foo: (bar baz)",
1539    tree: (justfile (recipe foo (deps (bar baz)))),
1540  }
1541
1542  test! {
1543    name: recipe_dependency_argument_concatenation,
1544    text: "foo: (bar 'a' + 'b' 'c' + 'd')",
1545    tree: (justfile (recipe foo (deps (bar (+ 'a' 'b') (+ 'c' 'd'))))),
1546  }
1547
1548  test! {
1549    name: recipe_subsequent,
1550    text: "foo: && bar",
1551    tree: (justfile (recipe foo (sups bar))),
1552  }
1553
1554  test! {
1555    name: recipe_line_single,
1556    text: "foo:\n bar",
1557    tree: (justfile (recipe foo (body ("bar")))),
1558  }
1559
1560  test! {
1561    name: recipe_line_multiple,
1562    text: "foo:\n bar\n baz\n {{\"bob\"}}biz",
1563    tree: (justfile (recipe foo (body ("bar") ("baz") (("bob") "biz")))),
1564  }
1565
1566  test! {
1567    name: recipe_line_interpolation,
1568    text: "foo:\n bar{{\"bob\"}}biz",
1569    tree: (justfile (recipe foo (body ("bar" ("bob") "biz")))),
1570  }
1571
1572  test! {
1573    name: comment,
1574    text: "# foo",
1575    tree: (justfile (comment "# foo")),
1576  }
1577
1578  test! {
1579    name: comment_before_alias,
1580    text: "# foo\nalias x := y",
1581    tree: (justfile (comment "# foo") (alias x y)),
1582  }
1583
1584  test! {
1585    name: comment_after_alias,
1586    text: "alias x := y # foo",
1587    tree: (justfile (alias x y)),
1588  }
1589
1590  test! {
1591    name: comment_assignment,
1592    text: "x := y # foo",
1593    tree: (justfile (assignment x y)),
1594  }
1595
1596  test! {
1597    name: comment_export,
1598    text: "export x := y # foo",
1599    tree: (justfile (assignment #export x y)),
1600  }
1601
1602  test! {
1603    name: comment_recipe,
1604    text: "foo: # bar",
1605    tree: (justfile (recipe foo)),
1606  }
1607
1608  test! {
1609    name: comment_recipe_dependencies,
1610    text: "foo: bar # baz",
1611    tree: (justfile (recipe foo (deps bar))),
1612  }
1613
1614  test! {
1615    name: doc_comment_single,
1616    text: "
1617      # foo
1618      bar:
1619    ",
1620    tree: (justfile (recipe "foo" bar)),
1621  }
1622
1623  test! {
1624    name: doc_comment_recipe_clear,
1625    text: "
1626      # foo
1627      bar:
1628      baz:
1629    ",
1630    tree: (justfile (recipe "foo" bar) (recipe baz)),
1631  }
1632
1633  test! {
1634    name: doc_comment_middle,
1635    text: "
1636      bar:
1637      # foo
1638      baz:
1639    ",
1640    tree: (justfile (recipe bar) (recipe "foo" baz)),
1641  }
1642
1643  test! {
1644    name: doc_comment_assignment_clear,
1645    text: "
1646      # foo
1647      x := y
1648      bar:
1649    ",
1650    tree: (justfile (comment "# foo") (assignment x y) (recipe bar)),
1651  }
1652
1653  test! {
1654    name: doc_comment_empty_line_clear,
1655    text: "
1656      # foo
1657
1658      bar:
1659    ",
1660    tree: (justfile (comment "# foo") (recipe bar)),
1661  }
1662
1663  test! {
1664    name: string_escape_tab,
1665    text: r#"x := "foo\tbar""#,
1666    tree: (justfile (assignment x "foo\tbar")),
1667  }
1668
1669  test! {
1670    name: string_escape_newline,
1671    text: r#"x := "foo\nbar""#,
1672    tree: (justfile (assignment x "foo\nbar")),
1673  }
1674
1675  test! {
1676    name: string_escape_suppress_newline,
1677    text: r#"
1678      x := "foo\
1679      bar"
1680    "#,
1681    tree: (justfile (assignment x "foobar")),
1682  }
1683
1684  test! {
1685    name: string_escape_carriage_return,
1686    text: r#"x := "foo\rbar""#,
1687    tree: (justfile (assignment x "foo\rbar")),
1688  }
1689
1690  test! {
1691    name: string_escape_slash,
1692    text: r#"x := "foo\\bar""#,
1693    tree: (justfile (assignment x "foo\\bar")),
1694  }
1695
1696  test! {
1697    name: string_escape_quote,
1698    text: r#"x := "foo\"bar""#,
1699    tree: (justfile (assignment x "foo\"bar")),
1700  }
1701
1702  test! {
1703    name: indented_string_raw_with_dedent,
1704    text: "
1705      x := '''
1706        foo\\t
1707        bar\\n
1708      '''
1709    ",
1710    tree: (justfile (assignment x "foo\\t\nbar\\n\n")),
1711  }
1712
1713  test! {
1714    name: indented_string_raw_no_dedent,
1715    text: "
1716      x := '''
1717      foo\\t
1718        bar\\n
1719      '''
1720    ",
1721    tree: (justfile (assignment x "foo\\t\n  bar\\n\n")),
1722  }
1723
1724  test! {
1725    name: indented_string_cooked,
1726    text: r#"
1727      x := """
1728        \tfoo\t
1729        \tbar\n
1730      """
1731    "#,
1732    tree: (justfile (assignment x "\tfoo\t\n\tbar\n\n")),
1733  }
1734
1735  test! {
1736    name: indented_string_cooked_no_dedent,
1737    text: r#"
1738      x := """
1739      \tfoo\t
1740        \tbar\n
1741      """
1742    "#,
1743    tree: (justfile (assignment x "\tfoo\t\n  \tbar\n\n")),
1744  }
1745
1746  test! {
1747    name: indented_backtick,
1748    text: r"
1749      x := ```
1750        \tfoo\t
1751        \tbar\n
1752      ```
1753    ",
1754    tree: (justfile (assignment x (backtick "\\tfoo\\t\n\\tbar\\n\n"))),
1755  }
1756
1757  test! {
1758    name: indented_backtick_no_dedent,
1759    text: r"
1760      x := ```
1761      \tfoo\t
1762        \tbar\n
1763      ```
1764    ",
1765    tree: (justfile (assignment x (backtick "\\tfoo\\t\n  \\tbar\\n\n"))),
1766  }
1767
1768  test! {
1769    name: recipe_variadic_with_default_after_default,
1770    text: r"
1771      f a=b +c=d:
1772    ",
1773    tree: (justfile (recipe f (params (a b) +(c d)))),
1774  }
1775
1776  test! {
1777    name: parameter_default_concatenation_variable,
1778    text: r#"
1779      x := "10"
1780
1781      f y=(`echo hello` + x) +z="foo":
1782    "#,
1783    tree: (justfile
1784      (assignment x "10")
1785      (recipe f (params (y ((+ (backtick "echo hello") x))) +(z "foo")))
1786    ),
1787  }
1788
1789  test! {
1790    name: parameter_default_multiple,
1791    text: r#"
1792      x := "10"
1793      f y=(`echo hello` + x) +z=("foo" + "bar"):
1794    "#,
1795    tree: (justfile
1796      (assignment x "10")
1797      (recipe f (params (y ((+ (backtick "echo hello") x))) +(z ((+ "foo" "bar")))))
1798    ),
1799  }
1800
1801  test! {
1802    name: parse_raw_string_default,
1803    text: r"
1804
1805      foo a='b\t':
1806
1807
1808    ",
1809    tree: (justfile (recipe foo (params (a "b\\t")))),
1810  }
1811
1812  test! {
1813    name: parse_alias_after_target,
1814    text: r"
1815      foo:
1816        echo a
1817      alias f := foo
1818    ",
1819    tree: (justfile
1820      (recipe foo (body ("echo a")))
1821      (alias f foo)
1822    ),
1823  }
1824
1825  test! {
1826    name: parse_alias_before_target,
1827    text: "
1828      alias f := foo
1829      foo:
1830        echo a
1831      ",
1832    tree: (justfile
1833      (alias f foo)
1834      (recipe foo (body ("echo a")))
1835    ),
1836  }
1837
1838  test! {
1839    name: parse_alias_with_comment,
1840    text: "
1841      alias f := foo #comment
1842      foo:
1843        echo a
1844    ",
1845    tree: (justfile
1846      (alias f foo)
1847      (recipe foo (body ("echo a")))
1848    ),
1849  }
1850
1851  test! {
1852    name: parse_assignment_with_comment,
1853    text: "
1854      f := foo #comment
1855      foo:
1856        echo a
1857    ",
1858    tree: (justfile
1859      (assignment f foo)
1860      (recipe foo (body ("echo a")))
1861    ),
1862  }
1863
1864  test! {
1865    name: parse_complex,
1866    text: "
1867      x:
1868      y:
1869      z:
1870      foo := \"xx\"
1871      bar := foo
1872      goodbye := \"y\"
1873      hello a b    c   : x y    z #hello
1874        #! blah
1875        #blarg
1876        {{ foo + bar}}abc{{ goodbye\t  + \"x\" }}xyz
1877        1
1878        2
1879        3
1880    ",
1881    tree: (justfile
1882      (recipe x)
1883      (recipe y)
1884      (recipe z)
1885      (assignment foo "xx")
1886      (assignment bar foo)
1887      (assignment goodbye "y")
1888      (recipe hello
1889        (params (a) (b) (c))
1890        (deps x y z)
1891        (body
1892          ("#! blah")
1893          ("#blarg")
1894          (((+ foo bar)) "abc" ((+ goodbye "x")) "xyz")
1895          ("1")
1896          ("2")
1897          ("3")
1898        )
1899      )
1900    ),
1901  }
1902
1903  test! {
1904    name: parse_shebang,
1905    text: "
1906      practicum := 'hello'
1907      install:
1908      \t#!/bin/sh
1909      \tif [[ -f {{practicum}} ]]; then
1910      \t\treturn
1911      \tfi
1912      ",
1913    tree: (justfile
1914      (assignment practicum "hello")
1915      (recipe install
1916        (body
1917         ("#!/bin/sh")
1918         ("if [[ -f " (practicum) " ]]; then")
1919         ("\treturn")
1920         ("fi")
1921        )
1922      )
1923    ),
1924  }
1925
1926  test! {
1927    name: parse_simple_shebang,
1928    text: "a:\n #!\n  print(1)",
1929    tree: (justfile
1930      (recipe a (body ("#!") (" print(1)")))
1931    ),
1932  }
1933
1934  test! {
1935    name: parse_assignments,
1936    text: r#"
1937      a := "0"
1938      c := a + b + a + b
1939      b := "1"
1940    "#,
1941    tree: (justfile
1942      (assignment a "0")
1943      (assignment c (+ a (+ b (+ a b))))
1944      (assignment b "1")
1945    ),
1946  }
1947
1948  test! {
1949    name: parse_assignment_backticks,
1950    text: "
1951      a := `echo hello`
1952      c := a + b + a + b
1953      b := `echo goodbye`
1954    ",
1955    tree: (justfile
1956      (assignment a (backtick "echo hello"))
1957      (assignment c (+ a (+ b (+ a b))))
1958      (assignment b (backtick "echo goodbye"))
1959    ),
1960  }
1961
1962  test! {
1963    name: parse_interpolation_backticks,
1964    text: r#"
1965      a:
1966        echo {{  `echo hello` + "blarg"   }} {{   `echo bob`   }}
1967    "#,
1968    tree: (justfile
1969      (recipe a
1970        (body ("echo " ((+ (backtick "echo hello") "blarg")) " " ((backtick "echo bob"))))
1971      )
1972    ),
1973  }
1974
1975  test! {
1976    name: eof_test,
1977    text: "x:\ny:\nz:\na b c: x y z",
1978    tree: (justfile
1979      (recipe x)
1980      (recipe y)
1981      (recipe z)
1982      (recipe a (params (b) (c)) (deps x y z))
1983    ),
1984  }
1985
1986  test! {
1987    name: string_quote_escape,
1988    text: r#"a := "hello\"""#,
1989    tree: (justfile
1990      (assignment a "hello\"")
1991    ),
1992  }
1993
1994  test! {
1995    name: string_escapes,
1996    text: r#"a := "\n\t\r\"\\""#,
1997    tree: (justfile (assignment a "\n\t\r\"\\")),
1998  }
1999
2000  test! {
2001    name: parameters,
2002    text: "
2003      a b c:
2004        {{b}} {{c}}
2005    ",
2006    tree: (justfile (recipe a (params (b) (c)) (body ((b) " " (c))))),
2007  }
2008
2009  test! {
2010    name: unary_functions,
2011    text: "
2012      x := arch()
2013
2014      a:
2015        {{os()}} {{os_family()}}
2016    ",
2017    tree: (justfile
2018      (assignment x (call arch))
2019      (recipe a (body (((call os)) " " ((call os_family)))))
2020    ),
2021  }
2022
2023  test! {
2024    name: env_functions,
2025    text: r#"
2026      x := env_var('foo',)
2027
2028      a:
2029        {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}
2030    "#,
2031    tree: (justfile
2032      (assignment x (call env_var "foo"))
2033      (recipe a
2034        (body
2035          (
2036            ((call env_var_or_default (+ "foo" "bar") "baz"))
2037            " "
2038            ((call env_var (call env_var "baz")))
2039          )
2040        )
2041      )
2042    ),
2043  }
2044
2045  test! {
2046    name: parameter_default_string,
2047    text: r#"
2048      f x="abc":
2049    "#,
2050    tree: (justfile (recipe f (params (x "abc")))),
2051  }
2052
2053  test! {
2054    name: parameter_default_raw_string,
2055    text: r"
2056      f x='abc':
2057    ",
2058    tree: (justfile (recipe f (params (x "abc")))),
2059  }
2060
2061  test! {
2062    name: parameter_default_backtick,
2063    text: "
2064      f x=`echo hello`:
2065    ",
2066    tree: (justfile
2067      (recipe f (params (x (backtick "echo hello"))))
2068    ),
2069  }
2070
2071  test! {
2072    name: parameter_default_concatenation_string,
2073    text: r#"
2074      f x=(`echo hello` + "foo"):
2075    "#,
2076    tree: (justfile (recipe f (params (x ((+ (backtick "echo hello") "foo")))))),
2077  }
2078
2079  test! {
2080    name: concatenation_in_group,
2081    text: "x := ('0' + '1')",
2082    tree: (justfile (assignment x ((+ "0" "1")))),
2083  }
2084
2085  test! {
2086    name: string_in_group,
2087    text: "x := ('0'   )",
2088    tree: (justfile (assignment x ("0"))),
2089  }
2090
2091  test! {
2092    name: escaped_dos_newlines,
2093    text: "
2094      @spam:\r
2095      \t{ \\\r
2096      \t\tfiglet test; \\\r
2097      \t\tcargo build --color always 2>&1; \\\r
2098      \t\tcargo test  --color always -- --color always 2>&1; \\\r
2099      \t} | less\r
2100    ",
2101    tree: (justfile
2102      (recipe #quiet spam
2103        (body
2104         ("{ \\")
2105         ("\tfiglet test; \\")
2106         ("\tcargo build --color always 2>&1; \\")
2107         ("\tcargo test  --color always -- --color always 2>&1; \\")
2108         ("} | less")
2109        )
2110      )
2111    ),
2112  }
2113
2114  test! {
2115    name: empty_body,
2116    text: "a:",
2117    tree: (justfile (recipe a)),
2118  }
2119
2120  test! {
2121    name: single_line_body,
2122    text: "a:\n foo",
2123    tree: (justfile (recipe a (body ("foo")))),
2124  }
2125
2126  test! {
2127    name: trimmed_body,
2128    text: "a:\n foo\n \n \n \nb:\n  ",
2129    tree: (justfile (recipe a (body ("foo"))) (recipe b)),
2130  }
2131
2132  test! {
2133    name: set_export_implicit,
2134    text: "set export",
2135    tree: (justfile (set export true)),
2136  }
2137
2138  test! {
2139    name: set_export_true,
2140    text: "set export := true",
2141    tree: (justfile (set export true)),
2142  }
2143
2144  test! {
2145    name: set_export_false,
2146    text: "set export := false",
2147    tree: (justfile (set export false)),
2148  }
2149
2150  test! {
2151    name: set_dotenv_load_implicit,
2152    text: "set dotenv-load",
2153    tree: (justfile (set dotenv_load true)),
2154  }
2155
2156  test! {
2157    name: set_allow_duplicate_recipes_implicit,
2158    text: "set allow-duplicate-recipes",
2159    tree: (justfile (set allow_duplicate_recipes true)),
2160  }
2161
2162  test! {
2163    name: set_allow_duplicate_variables_implicit,
2164    text: "set allow-duplicate-variables",
2165    tree: (justfile (set allow_duplicate_variables true)),
2166  }
2167
2168  test! {
2169    name: set_dotenv_load_true,
2170    text: "set dotenv-load := true",
2171    tree: (justfile (set dotenv_load true)),
2172  }
2173
2174  test! {
2175    name: set_dotenv_load_false,
2176    text: "set dotenv-load := false",
2177    tree: (justfile (set dotenv_load false)),
2178  }
2179
2180  test! {
2181    name: set_positional_arguments_implicit,
2182    text: "set positional-arguments",
2183    tree: (justfile (set positional_arguments true)),
2184  }
2185
2186  test! {
2187    name: set_positional_arguments_true,
2188    text: "set positional-arguments := true",
2189    tree: (justfile (set positional_arguments true)),
2190  }
2191
2192  test! {
2193    name: set_quiet_implicit,
2194    text: "set quiet",
2195    tree: (justfile (set quiet true)),
2196  }
2197
2198  test! {
2199    name: set_quiet_true,
2200    text: "set quiet := true",
2201    tree: (justfile (set quiet true)),
2202  }
2203
2204  test! {
2205    name: set_quiet_false,
2206    text: "set quiet := false",
2207    tree: (justfile (set quiet false)),
2208  }
2209
2210  test! {
2211    name: set_positional_arguments_false,
2212    text: "set positional-arguments := false",
2213    tree: (justfile (set positional_arguments false)),
2214  }
2215
2216  test! {
2217    name: set_shell_no_arguments,
2218    text: "set shell := ['tclsh']",
2219    tree: (justfile (set shell "tclsh")),
2220  }
2221
2222  test! {
2223    name: set_shell_no_arguments_cooked,
2224    text: "set shell := [\"tclsh\"]",
2225    tree: (justfile (set shell "tclsh")),
2226  }
2227
2228  test! {
2229    name: set_shell_no_arguments_trailing_comma,
2230    text: "set shell := ['tclsh',]",
2231    tree: (justfile (set shell "tclsh")),
2232  }
2233
2234  test! {
2235    name: set_shell_with_one_argument,
2236    text: "set shell := ['bash', '-cu']",
2237    tree: (justfile (set shell "bash" "-cu")),
2238  }
2239
2240  test! {
2241    name: set_shell_with_one_argument_trailing_comma,
2242    text: "set shell := ['bash', '-cu',]",
2243    tree: (justfile (set shell "bash" "-cu")),
2244  }
2245
2246  test! {
2247    name: set_shell_with_two_arguments,
2248    text: "set shell := ['bash', '-cu', '-l']",
2249    tree: (justfile (set shell "bash" "-cu" "-l")),
2250  }
2251
2252  test! {
2253    name: set_windows_powershell_implicit,
2254    text: "set windows-powershell",
2255    tree: (justfile (set windows_powershell true)),
2256  }
2257
2258  test! {
2259    name: set_windows_powershell_true,
2260    text: "set windows-powershell := true",
2261    tree: (justfile (set windows_powershell true)),
2262  }
2263
2264  test! {
2265    name: set_windows_powershell_false,
2266    text: "set windows-powershell := false",
2267    tree: (justfile (set windows_powershell false)),
2268  }
2269
2270  test! {
2271    name: set_working_directory,
2272    text: "set working-directory := 'foo'",
2273    tree: (justfile (set working_directory "foo")),
2274  }
2275
2276  test! {
2277    name: conditional,
2278    text: "a := if b == c { d } else { e }",
2279    tree: (justfile (assignment a (if b == c d e))),
2280  }
2281
2282  test! {
2283    name: conditional_inverted,
2284    text: "a := if b != c { d } else { e }",
2285    tree: (justfile (assignment a (if b != c d e))),
2286  }
2287
2288  test! {
2289    name: conditional_concatenations,
2290    text: "a := if b0 + b1 == c0 + c1 { d0 + d1 } else { e0 + e1 }",
2291    tree: (justfile (assignment a (if (+ b0 b1) == (+ c0 c1) (+ d0 d1) (+ e0 e1)))),
2292  }
2293
2294  test! {
2295    name: conditional_nested_lhs,
2296    text: "a := if if b == c { d } else { e } == c { d } else { e }",
2297    tree: (justfile (assignment a (if (if b == c d e) == c d e))),
2298  }
2299
2300  test! {
2301    name: conditional_nested_rhs,
2302    text: "a := if c == if b == c { d } else { e } { d } else { e }",
2303    tree: (justfile (assignment a (if c == (if b == c d e) d e))),
2304  }
2305
2306  test! {
2307    name: conditional_nested_then,
2308    text: "a := if b == c { if b == c { d } else { e } } else { e }",
2309    tree: (justfile (assignment a (if b == c (if b == c d e) e))),
2310  }
2311
2312  test! {
2313    name: conditional_nested_otherwise,
2314    text: "a := if b == c { d } else { if b == c { d } else { e } }",
2315    tree: (justfile (assignment a (if b == c d (if b == c d e)))),
2316  }
2317
2318  test! {
2319    name: import,
2320    text: "import \"some/file/path.txt\"     \n",
2321    tree: (justfile (import "some/file/path.txt")),
2322  }
2323
2324  test! {
2325    name: optional_import,
2326    text: "import? \"some/file/path.txt\"     \n",
2327    tree: (justfile (import ? "some/file/path.txt")),
2328  }
2329
2330  test! {
2331    name: module_with,
2332    text: "mod foo",
2333    tree: (justfile (mod foo )),
2334  }
2335
2336  test! {
2337    name: optional_module,
2338    text: "mod? foo",
2339    tree: (justfile (mod ? foo)),
2340  }
2341
2342  test! {
2343    name: module_with_path,
2344    text: "mod foo \"some/file/path.txt\"     \n",
2345    tree: (justfile (mod foo "some/file/path.txt")),
2346  }
2347
2348  test! {
2349    name: optional_module_with_path,
2350    text: "mod? foo \"some/file/path.txt\"     \n",
2351    tree: (justfile (mod ? foo "some/file/path.txt")),
2352  }
2353
2354  test! {
2355    name: assert,
2356    text: "a := assert(foo == \"bar\", \"error\")",
2357    tree: (justfile (assignment a (assert foo == "bar" "error"))),
2358  }
2359
2360  test! {
2361    name: assert_conditional_condition,
2362    text: "foo := assert(if a != b { c } else { d } == \"abc\", \"error\")",
2363    tree: (justfile (assignment foo (assert (if a != b c d) == "abc" "error"))),
2364  }
2365
2366  error! {
2367    name:   alias_syntax_multiple_rhs,
2368    input:  "alias foo := bar baz",
2369    offset: 17,
2370    line:   0,
2371    column: 17,
2372    width:  3,
2373    kind:   UnexpectedToken { expected: vec![Comment, Eof, Eol], found: Identifier },
2374  }
2375
2376  error! {
2377    name:   alias_syntax_no_rhs,
2378    input:  "alias foo := \n",
2379    offset: 13,
2380    line:   0,
2381    column: 13,
2382    width:  1,
2383    kind:   UnexpectedToken {expected: vec![Identifier], found:Eol},
2384  }
2385
2386  error! {
2387    name:   missing_colon,
2388    input:  "a b c\nd e f",
2389    offset:  5,
2390    line:   0,
2391    column: 5,
2392    width:  1,
2393    kind:   UnexpectedToken{
2394      expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],
2395      found:    Eol
2396    },
2397  }
2398
2399  error! {
2400    name:   missing_default_eol,
2401    input:  "hello arg=\n",
2402    offset:  10,
2403    line:   0,
2404    column: 10,
2405    width:  1,
2406    kind:   UnexpectedToken {
2407      expected: vec![
2408        Backtick,
2409        Identifier,
2410        ParenL,
2411        StringToken,
2412      ],
2413      found: Eol
2414    },
2415  }
2416
2417  error! {
2418    name:   missing_default_eof,
2419    input:  "hello arg=",
2420    offset:  10,
2421    line:   0,
2422    column: 10,
2423    width:  0,
2424    kind:   UnexpectedToken {
2425      expected: vec![
2426        Backtick,
2427        Identifier,
2428        ParenL,
2429        StringToken,
2430      ],
2431      found: Eof,
2432    },
2433  }
2434
2435  error! {
2436    name:   missing_eol,
2437    input:  "a b c: z =",
2438    offset:  9,
2439    line:    0,
2440    column:  9,
2441    width:   1,
2442    kind:    UnexpectedToken{
2443      expected: vec![AmpersandAmpersand, Comment, Eof, Eol, Identifier, ParenL],
2444      found: Equals
2445    },
2446  }
2447
2448  error! {
2449    name:   unexpected_brace,
2450    input:  "{{",
2451    offset:  0,
2452    line:   0,
2453    column: 0,
2454    width:  1,
2455    kind: UnexpectedToken {
2456      expected: vec![At, BracketL, Comment, Eof, Eol, Identifier],
2457      found: BraceL,
2458    },
2459  }
2460
2461  error! {
2462    name:   unclosed_parenthesis_in_expression,
2463    input:  "x := foo(",
2464    offset: 9,
2465    line:   0,
2466    column: 9,
2467    width:  0,
2468    kind: UnexpectedToken{
2469      expected: vec![
2470        Backtick,
2471        Identifier,
2472        ParenL,
2473        ParenR,
2474        Slash,
2475        StringToken,
2476      ],
2477      found: Eof,
2478    },
2479  }
2480
2481  error! {
2482    name:   unclosed_parenthesis_in_interpolation,
2483    input:  "a:\n echo {{foo(}}",
2484    offset:  15,
2485    line:   1,
2486    column: 12,
2487    width:  2,
2488    kind:   UnexpectedToken{
2489      expected: vec![
2490        Backtick,
2491        Identifier,
2492        ParenL,
2493        ParenR,
2494        Slash,
2495        StringToken,
2496      ],
2497      found: InterpolationEnd,
2498    },
2499  }
2500
2501  error! {
2502    name:   plus_following_parameter,
2503    input:  "a b c+:",
2504    offset: 6,
2505    line:   0,
2506    column: 6,
2507    width:  1,
2508    kind:   UnexpectedToken{expected: vec![Dollar, Identifier], found: Colon},
2509  }
2510
2511  error! {
2512    name:   invalid_escape_sequence,
2513    input:  r#"foo := "\b""#,
2514    offset: 7,
2515    line:   0,
2516    column: 7,
2517    width:  4,
2518    kind:   InvalidEscapeSequence{character: 'b'},
2519  }
2520
2521  error! {
2522    name:   bad_export,
2523    input:  "export a",
2524    offset:  8,
2525    line:   0,
2526    column: 8,
2527    width:  0,
2528    kind:   UnexpectedToken {
2529      expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],
2530      found:    Eof
2531    },
2532  }
2533
2534  error! {
2535    name:   parameter_follows_variadic_parameter,
2536    input:  "foo +a b:",
2537    offset: 7,
2538    line:   0,
2539    column: 7,
2540    width:  1,
2541    kind:   ParameterFollowsVariadicParameter{parameter: "b"},
2542  }
2543
2544  error! {
2545    name:   parameter_after_variadic,
2546    input:  "foo +a bbb:",
2547    offset: 7,
2548    line:   0,
2549    column: 7,
2550    width:  3,
2551    kind:   ParameterFollowsVariadicParameter{parameter: "bbb"},
2552  }
2553
2554  error! {
2555    name:   concatenation_in_default,
2556    input:  "foo a=c+d e:",
2557    offset: 10,
2558    line:   0,
2559    column: 10,
2560    width:  1,
2561    kind:   ParameterFollowsVariadicParameter{parameter: "e"},
2562  }
2563
2564  error! {
2565    name:   set_shell_empty,
2566    input:  "set shell := []",
2567    offset: 14,
2568    line:   0,
2569    column: 14,
2570    width:  1,
2571    kind:   UnexpectedToken {
2572      expected: vec![
2573        Identifier,
2574        StringToken,
2575      ],
2576      found: BracketR,
2577    },
2578  }
2579
2580  error! {
2581    name:   set_shell_non_literal_first,
2582    input:  "set shell := ['bar' + 'baz']",
2583    offset: 20,
2584    line:   0,
2585    column: 20,
2586    width:  1,
2587    kind:   UnexpectedToken {
2588      expected: vec![BracketR, Comma],
2589      found: Plus,
2590    },
2591  }
2592
2593  error! {
2594    name:   set_shell_non_literal_second,
2595    input:  "set shell := ['biz', 'bar' + 'baz']",
2596    offset: 27,
2597    line:   0,
2598    column: 27,
2599    width:  1,
2600    kind:   UnexpectedToken {
2601      expected: vec![BracketR, Comma],
2602      found: Plus,
2603    },
2604  }
2605
2606  error! {
2607    name:   set_shell_bad_comma,
2608    input:  "set shell := ['bash',",
2609    offset: 21,
2610    line:   0,
2611    column: 21,
2612    width:  0,
2613    kind:   UnexpectedToken {
2614      expected: vec![
2615        BracketR,
2616        Identifier,
2617        StringToken,
2618      ],
2619      found: Eof,
2620    },
2621  }
2622
2623  error! {
2624    name:   set_shell_bad,
2625    input:  "set shell := ['bash'",
2626    offset: 20,
2627    line:   0,
2628    column: 20,
2629    width:  0,
2630    kind:   UnexpectedToken {
2631      expected: vec![BracketR, Comma],
2632      found: Eof,
2633    },
2634  }
2635
2636  error! {
2637    name:   empty_attribute,
2638    input:  "[]\nsome_recipe:\n @exit 3",
2639    offset: 1,
2640    line:   0,
2641    column: 1,
2642    width:  1,
2643    kind:   UnexpectedToken {
2644      expected: vec![Identifier],
2645      found: BracketR,
2646    },
2647  }
2648
2649  error! {
2650    name:   unknown_attribute,
2651    input:  "[unknown]\nsome_recipe:\n @exit 3",
2652    offset: 1,
2653    line:   0,
2654    column: 1,
2655    width:  7,
2656    kind:   UnknownAttribute { attribute: "unknown" },
2657  }
2658
2659  error! {
2660    name:   set_unknown,
2661    input:  "set shall := []",
2662    offset: 4,
2663    line:   0,
2664    column: 4,
2665    width:  5,
2666    kind:   UnknownSetting {
2667      setting: "shall",
2668    },
2669  }
2670
2671  error! {
2672    name:   set_shell_non_string,
2673    input:  "set shall := []",
2674    offset: 4,
2675    line:   0,
2676    column: 4,
2677    width:  5,
2678    kind:   UnknownSetting {
2679      setting: "shall",
2680    },
2681  }
2682
2683  error! {
2684    name:   unknown_function,
2685    input:  "a := foo()",
2686    offset: 5,
2687    line:   0,
2688    column: 5,
2689    width:  3,
2690    kind:   UnknownFunction{function: "foo"},
2691  }
2692
2693  error! {
2694    name:   unknown_function_in_interpolation,
2695    input:  "a:\n echo {{bar()}}",
2696    offset: 11,
2697    line:   1,
2698    column: 8,
2699    width:  3,
2700    kind:   UnknownFunction{function: "bar"},
2701  }
2702
2703  error! {
2704    name:   unknown_function_in_default,
2705    input:  "a f=baz():",
2706    offset: 4,
2707    line:   0,
2708    column: 4,
2709    width:  3,
2710    kind:   UnknownFunction{function: "baz"},
2711  }
2712
2713  error! {
2714    name: function_argument_count_nullary,
2715    input: "x := arch('foo')",
2716    offset: 5,
2717    line: 0,
2718    column: 5,
2719    width: 4,
2720    kind: FunctionArgumentCountMismatch {
2721      function: "arch",
2722      found: 1,
2723      expected: 0..=0,
2724    },
2725  }
2726
2727  error! {
2728    name: function_argument_count_unary,
2729    input: "x := env_var()",
2730    offset: 5,
2731    line: 0,
2732    column: 5,
2733    width: 7,
2734    kind: FunctionArgumentCountMismatch {
2735      function: "env_var",
2736      found: 0,
2737      expected: 1..=1,
2738    },
2739  }
2740
2741  error! {
2742    name: function_argument_count_too_high_unary_opt,
2743    input: "x := env('foo', 'foo', 'foo')",
2744    offset: 5,
2745    line: 0,
2746    column: 5,
2747    width: 3,
2748    kind: FunctionArgumentCountMismatch {
2749      function: "env",
2750      found: 3,
2751      expected: 1..=2,
2752    },
2753  }
2754
2755  error! {
2756    name: function_argument_count_too_low_unary_opt,
2757    input: "x := env()",
2758    offset: 5,
2759    line: 0,
2760    column: 5,
2761    width: 3,
2762    kind: FunctionArgumentCountMismatch {
2763      function: "env",
2764      found: 0,
2765      expected: 1..=2,
2766    },
2767  }
2768
2769  error! {
2770    name: function_argument_count_binary,
2771    input: "x := env_var_or_default('foo')",
2772    offset: 5,
2773    line: 0,
2774    column: 5,
2775    width: 18,
2776    kind: FunctionArgumentCountMismatch {
2777      function: "env_var_or_default",
2778      found: 1,
2779      expected: 2..=2,
2780    },
2781  }
2782
2783  error! {
2784    name: function_argument_count_binary_plus,
2785    input: "x := join('foo')",
2786    offset: 5,
2787    line: 0,
2788    column: 5,
2789    width: 4,
2790    kind: FunctionArgumentCountMismatch {
2791      function: "join",
2792      found: 1,
2793      expected: 2..=usize::MAX,
2794    },
2795  }
2796
2797  error! {
2798    name: function_argument_count_ternary,
2799    input: "x := replace('foo')",
2800    offset: 5,
2801    line: 0,
2802    column: 5,
2803    width: 7,
2804    kind: FunctionArgumentCountMismatch {
2805      function: "replace",
2806      found: 1,
2807      expected: 3..=3,
2808    },
2809  }
2810}