1use {super::*, TokenKind::*};
2
3pub 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 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 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 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 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 fn next_is(&mut self, kind: TokenKind) -> bool {
104 self.next_are(&[kind])
105 }
106
107 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 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 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 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 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 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 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 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 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 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 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 fn accepted(&mut self, kind: TokenKind) -> CompileResult<'src, bool> {
293 Ok(self.accept(kind)?.is_some())
294 }
295
296 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 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 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 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 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 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 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 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 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 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 fn parse_name(&mut self) -> CompileResult<'src, Name<'src>> {
839 self.expect(Identifier).map(Name::from_identifier)
840 }
841
842 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 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 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 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 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 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 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 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}