1use super::lex::Operator;
20use crate::source::Location;
21use crate::source::pretty::{
22 Footnote, FootnoteType, Report, ReportType, Snippet, Span, SpanRole, add_span,
23};
24use crate::syntax::AndOr;
25use crate::syntax::RedirOp;
26use std::borrow::Cow;
27use std::rc::Rc;
28use thiserror::Error;
29
30#[derive(Clone, Debug, Eq, Error, PartialEq)]
32#[error("{}", self.message())]
33#[non_exhaustive]
34pub enum SyntaxError {
35 IncompleteEscape,
37 InvalidEscape,
39 UnclosedParen { opening_location: Location },
41 UnclosedSingleQuote { opening_location: Location },
43 UnclosedDoubleQuote { opening_location: Location },
45 UnclosedDollarSingleQuote { opening_location: Location },
47 UnclosedParam { opening_location: Location },
49 EmptyParam,
51 InvalidParam,
53 InvalidModifier,
55 MultipleModifier,
57 UnclosedCommandSubstitution { opening_location: Location },
59 UnclosedBackquote { opening_location: Location },
61 UnclosedArith { opening_location: Location },
63 InvalidCommandToken,
65 MissingSeparator,
67 FdOutOfRange,
69 InvalidIoLocation,
71 MissingRedirOperand,
73 MissingHereDocDelimiter,
75 MissingHereDocContent,
77 UnclosedHereDocContent { redir_op_location: Location },
79 UnclosedArrayValue { opening_location: Location },
81 UnopenedGrouping,
83 UnclosedGrouping { opening_location: Location },
85 EmptyGrouping,
87 UnopenedSubshell,
89 UnclosedSubshell { opening_location: Location },
91 EmptySubshell,
93 UnopenedLoop,
95 UnopenedDoClause,
97 UnclosedDoClause { opening_location: Location },
99 EmptyDoClause,
101 MissingForName,
103 InvalidForName,
105 InvalidForValue,
107 MissingForBody { opening_location: Location },
109 UnclosedWhileClause { opening_location: Location },
111 EmptyWhileCondition,
113 UnclosedUntilClause { opening_location: Location },
115 EmptyUntilCondition,
117 IfMissingThen { if_location: Location },
119 EmptyIfCondition,
121 EmptyIfBody,
123 ElifMissingThen { elif_location: Location },
125 EmptyElifCondition,
127 EmptyElifBody,
129 EmptyElse,
131 UnopenedIf,
133 UnclosedIf { opening_location: Location },
135 MissingCaseSubject,
137 InvalidCaseSubject,
139 MissingIn { opening_location: Location },
141 UnclosedPatternList,
143 MissingPattern,
145 InvalidPattern,
147 #[deprecated(since = "0.12.1", note = "this error no longer occurs")]
149 EsacAsPattern,
150 UnopenedCase,
152 UnclosedCase { opening_location: Location },
154 UnmatchedParenthesis,
156 MissingFunctionBody,
158 InvalidFunctionBody,
160 InAsCommandName,
162 CloseBracketBracketAsCommandName,
164 MissingPipeline(AndOr),
166 DoubleNegation,
168 BangAfterBar,
170 MissingCommandAfterBang,
172 MissingCommandAfterBar,
174 RedundantToken,
176 IncompleteControlEscape,
178 IncompleteControlBackslashEscape,
180 InvalidControlEscape,
182 OctalEscapeOutOfRange,
184 IncompleteHexEscape,
186 IncompleteShortUnicodeEscape,
188 IncompleteLongUnicodeEscape,
190 UnicodeEscapeOutOfRange,
192 UnsupportedFunctionDefinitionSyntax,
194 UnsupportedDoubleBracketCommand,
196 UnsupportedNamespaceCommand,
198 UnsupportedSelectCommand,
200 UnsupportedProcessRedirection,
202 UnsupportedArithmeticCommand,
209 UnsupportedExtendedGlob,
216 NonPortableCaseTerminator(Operator),
221 NonPortableRedirOperator(RedirOp),
226 IoTokenAsRedirOperand,
235 MissingSeparatorBeforeReservedWord,
245 ColonSuffixedCommandName,
252 NonPortableEscape,
258 TooLongHexEscape,
264 NonPortableForName,
273 NonPortableFunctionName,
282 NonPortableAssignmentName,
290 ArrayAssignment,
296 SpecialBuiltinFunctionName,
304 NonPortableParamModifier,
312}
313
314impl SyntaxError {
315 #[must_use]
317 pub fn message(&self) -> &'static str {
318 use SyntaxError::*;
319 match self {
320 IncompleteEscape => "the backslash is escaping nothing",
321 InvalidEscape => "the backslash escape is invalid",
322 UnclosedParen { .. } => "the parenthesis is not closed",
323 UnclosedSingleQuote { .. } => "the single quote is not closed",
324 UnclosedDoubleQuote { .. } => "the double quote is not closed",
325 UnclosedDollarSingleQuote { .. } => "the dollar single quote is not closed",
326 UnclosedParam { .. } => "the parameter expansion is not closed",
327 EmptyParam => "the parameter name is missing",
328 InvalidParam => "the parameter name is invalid",
329 InvalidModifier => "the parameter expansion contains a malformed modifier",
330 MultipleModifier => "a suffix modifier cannot be used together with a prefix modifier",
331 UnclosedCommandSubstitution { .. } => "the command substitution is not closed",
332 UnclosedBackquote { .. } => "the backquote is not closed",
333 UnclosedArith { .. } => "the arithmetic expansion is not closed",
334 InvalidCommandToken => "the command starts with an inappropriate token",
335 MissingSeparator => "a separator is missing between the commands",
336 FdOutOfRange => "the file descriptor is too large",
337 InvalidIoLocation => "the I/O location prefix is not valid",
338 MissingRedirOperand => "the redirection operator is missing its operand",
339 MissingHereDocDelimiter => "the here-document operator is missing its delimiter",
340 MissingHereDocContent => "content of the here-document is missing",
341 UnclosedHereDocContent { .. } => {
342 "the delimiter to close the here-document content is missing"
343 }
344 UnclosedArrayValue { .. } => "the array assignment value is not closed",
345 UnopenedGrouping
346 | UnopenedSubshell
347 | UnopenedLoop
348 | UnopenedDoClause
349 | UnopenedIf
350 | UnopenedCase
351 | InAsCommandName
352 | CloseBracketBracketAsCommandName => "the compound command delimiter is unmatched",
353 UnclosedGrouping { .. } => "the grouping is not closed",
354 EmptyGrouping => "the grouping is missing its content",
355 UnclosedSubshell { .. } => "the subshell is not closed",
356 EmptySubshell => "the subshell is missing its content",
357 UnclosedDoClause { .. } => "the `do` clause is missing its closing `done`",
358 EmptyDoClause => "the `do` clause is missing its content",
359 MissingForName => "the variable name is missing in the `for` loop",
360 InvalidForName => "the variable name is invalid",
361 InvalidForValue => "the operator token is invalid in the word list of the `for` loop",
362 MissingForBody { .. } => "the `for` loop is missing its `do` clause",
363 UnclosedWhileClause { .. } => "the `while` loop is missing its `do` clause",
364 EmptyWhileCondition => "the `while` loop is missing its condition",
365 UnclosedUntilClause { .. } => "the `until` loop is missing its `do` clause",
366 EmptyUntilCondition => "the `until` loop is missing its condition",
367 IfMissingThen { .. } => "the `if` command is missing the `then` clause",
368 EmptyIfCondition => "the `if` command is missing its condition",
369 EmptyIfBody => "the `if` command is missing its body",
370 ElifMissingThen { .. } => "the `elif` clause is missing the `then` clause",
371 EmptyElifCondition => "the `elif` clause is missing its condition",
372 EmptyElifBody => "the `elif` clause is missing its body",
373 EmptyElse => "the `else` clause is missing its content",
374 UnclosedIf { .. } => "the `if` command is missing its closing `fi`",
375 MissingCaseSubject => "the subject is missing after `case`",
376 InvalidCaseSubject => "the `case` command subject is not a valid word",
377 MissingIn { .. } => "`in` is missing in the `case` command",
378 UnclosedPatternList => "the pattern list is not properly closed by a `)`",
379 MissingPattern => "a pattern is missing in the `case` command",
380 InvalidPattern => "the pattern is not a valid word token",
381 #[allow(deprecated, reason = "for backward compatible API")]
382 EsacAsPattern => "`esac` cannot be the first of a pattern list",
383 UnclosedCase { .. } => "the `case` command is missing its closing `esac`",
384 UnmatchedParenthesis => "`)` is missing after `(`",
385 MissingFunctionBody => "the function body is missing",
386 InvalidFunctionBody => "the function body must be a compound command",
387 MissingPipeline(AndOr::AndThen) => "a command is missing after `&&`",
388 MissingPipeline(AndOr::OrElse) => "a command is missing after `||`",
389 DoubleNegation => "`!` cannot be used twice in a row",
390 BangAfterBar => "`!` cannot be used in the middle of a pipeline",
391 MissingCommandAfterBang => "a command is missing after `!`",
392 MissingCommandAfterBar => "a command is missing after `|`",
393 RedundantToken => "there is a redundant token",
394 IncompleteControlEscape => "the control escape is incomplete",
395 IncompleteControlBackslashEscape => "the control-backslash escape is incomplete",
396 InvalidControlEscape => "the control escape is invalid",
397 OctalEscapeOutOfRange => "the octal escape is out of range",
398 IncompleteHexEscape => "the hexadecimal escape is incomplete",
399 IncompleteShortUnicodeEscape | IncompleteLongUnicodeEscape => {
400 "the Unicode escape is incomplete"
401 }
402 UnicodeEscapeOutOfRange => "the Unicode escape is out of range",
403 UnsupportedFunctionDefinitionSyntax
404 | UnsupportedDoubleBracketCommand
405 | UnsupportedNamespaceCommand
406 | UnsupportedSelectCommand
407 | UnsupportedProcessRedirection => "unsupported syntax",
408 UnsupportedArithmeticCommand => "`((` is ambiguous at the start of a command",
409 UnsupportedExtendedGlob => "`!(` is ambiguous at the start of a command",
410 NonPortableCaseTerminator(_) => "the case terminator is not portable",
411 NonPortableRedirOperator(_) => "the redirection operator is not portable",
412 IoTokenAsRedirOperand => {
413 "the redirection operand is missing because the token belongs to the next redirection"
414 }
415 MissingSeparatorBeforeReservedWord => "a separator is missing before the reserved word",
416 ColonSuffixedCommandName => "the command name is not portable",
417 NonPortableEscape => "the escape sequence is not portable",
418 TooLongHexEscape => "more than two hexadecimal digits follow `\\x`",
419 NonPortableForName => "the for loop variable name is not portable",
420 NonPortableFunctionName => "the function name is not portable",
421 NonPortableAssignmentName => "the assignment name is not portable",
422 ArrayAssignment => "arrays are not portable",
423 SpecialBuiltinFunctionName => {
424 "the function name is the same as a special built-in utility"
425 }
426 NonPortableParamModifier => "the parameter expansion is not portable",
427 }
428 }
429
430 #[must_use]
432 pub fn label(&self) -> &'static str {
433 use SyntaxError::*;
434 match self {
435 IncompleteEscape => "expected an escaped character after the backslash",
436 InvalidEscape => "invalid escape sequence",
437 UnclosedParen { .. }
438 | UnclosedCommandSubstitution { .. }
439 | UnclosedArrayValue { .. }
440 | UnclosedSubshell { .. }
441 | UnclosedPatternList
442 | UnmatchedParenthesis => "expected `)`",
443 EmptyGrouping
444 | EmptySubshell
445 | EmptyDoClause
446 | EmptyWhileCondition
447 | EmptyUntilCondition
448 | EmptyIfCondition
449 | EmptyIfBody
450 | EmptyElifCondition
451 | EmptyElifBody
452 | EmptyElse
453 | MissingPipeline(_)
454 | MissingCommandAfterBang
455 | MissingCommandAfterBar => "expected a command",
456 InvalidForValue | MissingCaseSubject | InvalidCaseSubject | MissingPattern
457 | InvalidPattern => "expected a word",
458 UnclosedSingleQuote { .. } | UnclosedDollarSingleQuote { .. } => "expected `'`",
459 UnclosedDoubleQuote { .. } => "expected `\"`",
460 UnclosedParam { .. } | UnclosedGrouping { .. } => "expected `}`",
461 EmptyParam => "expected a parameter name",
462 InvalidParam => "not a valid named or positional parameter",
463 InvalidModifier => "broken modifier",
464 MultipleModifier => "conflicting modifier",
465 UnclosedBackquote { .. } => "expected '`'",
466 UnclosedArith { .. } => "expected `))`",
467 InvalidCommandToken => "does not begin a valid command",
468 MissingSeparator => "expected `;` or `&` before this token",
469 FdOutOfRange => "unsupported file descriptor",
470 InvalidIoLocation => "unsupported I/O location prefix",
471 MissingRedirOperand => "expected a redirection operand",
472 MissingHereDocDelimiter => "expected a delimiter word",
473 MissingHereDocContent => "content not found",
474 UnclosedHereDocContent { .. } => "missing delimiter",
475 UnopenedGrouping => "no grouping command to close",
476 UnopenedSubshell => "no subshell to close",
477 UnopenedLoop => "not in a loop",
478 UnopenedDoClause => "no `do` clause to close",
479 UnclosedDoClause { .. } => "expected `done`",
480 MissingForName => "expected a variable name",
481 InvalidForName => "not a valid variable name",
482 MissingForBody { .. } | UnclosedWhileClause { .. } | UnclosedUntilClause { .. } => {
483 "expected `do ... done`"
484 }
485 IfMissingThen { .. } | ElifMissingThen { .. } => "expected `then ... fi`",
486 UnopenedIf => "not in an `if` command",
487 UnclosedIf { .. } => "expected `fi`",
488 MissingIn { .. } => "expected `in`",
489 #[allow(deprecated, reason = "for backward compatible API")]
490 EsacAsPattern => "needs quoting",
491 UnopenedCase => "not in a `case` command",
492 UnclosedCase { .. } => "expected `esac`",
493 MissingFunctionBody | InvalidFunctionBody => "expected a compound command",
494 InAsCommandName | CloseBracketBracketAsCommandName => {
495 "cannot be used as a command name"
496 }
497 DoubleNegation => "only one `!` allowed",
498 BangAfterBar => "`!` not allowed here",
499 RedundantToken => "unexpected token",
500 IncompleteControlEscape => r"expected a control character after `\c`",
501 IncompleteControlBackslashEscape => r"expected another backslash after `\c\`",
502 InvalidControlEscape => "not a valid control character",
503 OctalEscapeOutOfRange => r"expected a value between \0 and \377",
504 IncompleteHexEscape => r"expected a hexadecimal digit after `\x`",
505 IncompleteShortUnicodeEscape => r"expected a hexadecimal digit after `\u`",
506 IncompleteLongUnicodeEscape => r"expected a hexadecimal digit after `\U`",
507 UnicodeEscapeOutOfRange => "not a valid Unicode scalar value",
508 UnsupportedFunctionDefinitionSyntax => "the `function` keyword is not yet supported",
509 UnsupportedDoubleBracketCommand => "the `[[ ... ]]` command is not yet supported",
510 UnsupportedNamespaceCommand => "the `namespace` command is not yet supported",
511 UnsupportedSelectCommand => "the `select` command is not yet supported",
512 UnsupportedProcessRedirection => "process redirection is not yet supported",
513 UnsupportedArithmeticCommand => {
514 "other shells read this as an arithmetic command; insert a space for nested subshells"
515 }
516 UnsupportedExtendedGlob => {
517 "other shells read this as an extended glob; insert a space after `!` for a negated subshell"
518 }
519 NonPortableCaseTerminator(Operator::SemicolonSemicolonAnd) => {
520 "`;;&` is not a POSIX case terminator"
521 }
522 NonPortableCaseTerminator(Operator::SemicolonBar) => {
523 "`;|` is not a POSIX case terminator"
524 }
525 NonPortableCaseTerminator(_) => "not a POSIX case terminator",
526 NonPortableRedirOperator(RedirOp::Pipe) => "`>>|` is not a POSIX redirection operator",
527 NonPortableRedirOperator(RedirOp::String) => {
528 "`<<<` is not a POSIX redirection operator"
529 }
530 NonPortableRedirOperator(_) => "not a POSIX redirection operator",
531 IoTokenAsRedirOperand => "add a space before the following redirection operator",
532 MissingSeparatorBeforeReservedWord => {
533 "insert `;` or a newline before this reserved word"
534 }
535 ColonSuffixedCommandName => "a command name ending with `:` is reserved by POSIX",
536 NonPortableEscape => "not a POSIX escape sequence",
537 TooLongHexEscape => "use at most two hexadecimal digits",
538 NonPortableForName => "not a POSIX variable name",
539 NonPortableFunctionName => "not a POSIX name",
540 NonPortableAssignmentName => "not a POSIX variable name",
541 ArrayAssignment => "arrays are not specified by POSIX",
542 SpecialBuiltinFunctionName => "conflicts with a special built-in utility",
543 NonPortableParamModifier => {
544 "POSIX leaves this parameter/modifier combination unspecified"
545 }
546 }
547 }
548
549 #[must_use]
555 pub fn footnotes(&self) -> &'static [(FootnoteType, &'static str)] {
556 use SyntaxError::*;
557 match self {
558 BangAfterBar => &[(
559 FootnoteType::Suggestion,
560 "surround the pipeline component in a grouping: `{ ! ...; }`",
562 )],
563 UnsupportedArithmeticCommand
564 | UnsupportedExtendedGlob
565 | NonPortableCaseTerminator(_)
566 | NonPortableRedirOperator(_)
567 | IoTokenAsRedirOperand
568 | MissingSeparatorBeforeReservedWord
569 | ColonSuffixedCommandName
570 | NonPortableEscape
571 | TooLongHexEscape
572 | NonPortableForName
573 | NonPortableFunctionName
574 | NonPortableAssignmentName
575 | ArrayAssignment
576 | SpecialBuiltinFunctionName
577 | NonPortableParamModifier => &[(
578 FootnoteType::Note,
579 "this error is reported because the `portable` shell option is enabled",
580 )],
581 _ => &[],
582 }
583 }
584
585 #[must_use]
588 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
589 use SyntaxError::*;
590 match self {
591 UnclosedParen { opening_location }
592 | UnclosedSubshell { opening_location }
593 | UnclosedArrayValue { opening_location } => {
594 Some((opening_location, "the opening parenthesis was here"))
595 }
596 UnclosedSingleQuote { opening_location }
597 | UnclosedDoubleQuote { opening_location }
598 | UnclosedDollarSingleQuote { opening_location } => {
599 Some((opening_location, "the opening quote was here"))
600 }
601 UnclosedParam { opening_location } => {
602 Some((opening_location, "the parameter started here"))
603 }
604 UnclosedCommandSubstitution { opening_location } => {
605 Some((opening_location, "the command substitution started here"))
606 }
607 UnclosedBackquote { opening_location } => {
608 Some((opening_location, "the opening backquote was here"))
609 }
610 UnclosedArith { opening_location } => {
611 Some((opening_location, "the arithmetic expansion started here"))
612 }
613 UnclosedHereDocContent { redir_op_location } => {
614 Some((redir_op_location, "the redirection operator was here"))
615 }
616 UnclosedGrouping { opening_location } => {
617 Some((opening_location, "the opening brace was here"))
618 }
619 UnclosedDoClause { opening_location } => {
620 Some((opening_location, "the `do` clause started here"))
621 }
622 MissingForBody { opening_location } => {
623 Some((opening_location, "the `for` loop started here"))
624 }
625 UnclosedWhileClause { opening_location } => {
626 Some((opening_location, "the `while` loop started here"))
627 }
628 UnclosedUntilClause { opening_location } => {
629 Some((opening_location, "the `until` loop started here"))
630 }
631 IfMissingThen { if_location }
632 | UnclosedIf {
633 opening_location: if_location,
634 } => Some((if_location, "the `if` command started here")),
635 ElifMissingThen { elif_location } => {
636 Some((elif_location, "the `elif` clause started here"))
637 }
638 MissingIn { opening_location } | UnclosedCase { opening_location } => {
639 Some((opening_location, "the `case` command started here"))
640 }
641 _ => None,
642 }
643 }
644}
645
646#[derive(Clone, Debug, Error)]
648#[error("{}", self.message())]
649pub enum ErrorCause {
650 Io(#[from] Rc<std::io::Error>),
652 Syntax(#[from] SyntaxError),
654}
655
656impl PartialEq for ErrorCause {
657 fn eq(&self, other: &Self) -> bool {
658 match (self, other) {
659 (ErrorCause::Syntax(e1), ErrorCause::Syntax(e2)) => e1 == e2,
660 _ => false,
661 }
662 }
663}
664
665impl ErrorCause {
666 #[must_use]
668 pub fn message(&self) -> Cow<'static, str> {
669 use ErrorCause::*;
670 match self {
671 Io(e) => format!("cannot read commands: {e}").into(),
672 Syntax(e) => e.message().into(),
673 }
674 }
675
676 #[must_use]
678 pub fn label(&self) -> &'static str {
679 use ErrorCause::*;
680 match self {
681 Io(_) => "the command could be read up to here",
682 Syntax(e) => e.label(),
683 }
684 }
685
686 #[must_use]
690 pub fn footnotes(&self) -> &'static [(FootnoteType, &'static str)] {
691 use ErrorCause::*;
692 match self {
693 Io(_) => &[],
694 Syntax(e) => e.footnotes(),
695 }
696 }
697
698 #[must_use]
701 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
702 use ErrorCause::*;
703 match self {
704 Io(_) => None,
705 Syntax(e) => e.related_location(),
706 }
707 }
708}
709
710impl From<std::io::Error> for ErrorCause {
711 fn from(e: std::io::Error) -> ErrorCause {
712 ErrorCause::from(Rc::new(e))
713 }
714}
715
716#[derive(Clone, Debug, Error, PartialEq)]
718#[error("{cause}")]
719pub struct Error {
720 pub cause: ErrorCause,
721 pub location: Location,
722}
723
724impl Error {
725 #[must_use]
732 pub fn to_report(&self) -> Report<'_> {
733 let mut report = Report::new();
734 report.r#type = ReportType::Error;
735 report.title = self.cause.message();
736 report.snippets = Snippet::with_primary_span(&self.location, self.cause.label().into());
737
738 if let Some((location, label)) = self.cause.related_location() {
739 let label = label.into();
740 let span = Span {
741 range: location.byte_range(),
742 role: SpanRole::Supplementary { label },
743 };
744 add_span(&location.code, span, &mut report.snippets);
745 }
746
747 report.footnotes.extend(
748 self.cause
749 .footnotes()
750 .iter()
751 .map(|&(r#type, label)| Footnote {
752 r#type,
753 label: label.into(),
754 }),
755 );
756
757 report
758 }
759}
760
761impl<'a> From<&'a Error> for Report<'a> {
763 #[inline(always)]
764 fn from(error: &'a Error) -> Self {
765 error.to_report()
766 }
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772 use crate::source::Code;
773 use crate::source::Source;
774 use std::assert_matches;
775 use std::num::NonZeroU64;
776 use std::rc::Rc;
777
778 #[test]
779 fn display_for_error() {
780 let code = Rc::new(Code {
781 value: "".to_string().into(),
782 start_line_number: NonZeroU64::new(1).unwrap(),
783 source: Source::Unknown.into(),
784 });
785 let location = Location { code, range: 0..42 };
786 let error = Error {
787 cause: SyntaxError::MissingHereDocDelimiter.into(),
788 location,
789 };
790 assert_eq!(
791 error.to_string(),
792 "the here-document operator is missing its delimiter"
793 );
794 }
795
796 #[test]
797 fn from_error_for_report() {
798 let code = Rc::new(Code {
799 value: "!!!".to_string().into(),
800 start_line_number: NonZeroU64::new(1).unwrap(),
801 source: Source::Unknown.into(),
802 });
803 let error = Error {
804 cause: SyntaxError::MissingHereDocDelimiter.into(),
805 location: Location { code, range: 0..42 },
806 };
807
808 let report = Report::from(&error);
809
810 assert_eq!(report.r#type, ReportType::Error);
811 assert_eq!(
812 report.title,
813 "the here-document operator is missing its delimiter"
814 );
815 assert_eq!(report.snippets.len(), 1);
816 assert_eq!(*report.snippets[0].code.value.borrow(), "!!!");
817 assert_eq!(report.snippets[0].spans.len(), 1);
818 assert_eq!(report.snippets[0].spans[0].range, 0..3);
819 assert_matches!(
820 &report.snippets[0].spans[0].role,
821 SpanRole::Primary { label } if label == "expected a delimiter word"
822 );
823 assert_eq!(report.footnotes, []);
824 }
825
826 #[test]
827 fn footnotes_for_syntax_error() {
828 let note = (
830 FootnoteType::Note,
831 "this error is reported because the `portable` shell option is enabled",
832 );
833 assert_eq!(SyntaxError::IoTokenAsRedirOperand.footnotes(), [note]);
834 assert_eq!(
835 SyntaxError::UnsupportedArithmeticCommand.footnotes(),
836 [note]
837 );
838 assert_eq!(
840 SyntaxError::BangAfterBar.footnotes(),
841 [(
842 FootnoteType::Suggestion,
843 "surround the pipeline component in a grouping: `{ ! ...; }`"
844 )]
845 );
846 assert_eq!(SyntaxError::MissingHereDocDelimiter.footnotes(), []);
848 assert_eq!(SyntaxError::UnsupportedDoubleBracketCommand.footnotes(), []);
850 }
851
852 #[test]
853 fn report_has_footnote_for_portable_error() {
854 let code = Rc::new(Code {
855 value: "((:))".to_string().into(),
856 start_line_number: NonZeroU64::new(1).unwrap(),
857 source: Source::Unknown.into(),
858 });
859 let error = Error {
860 cause: SyntaxError::UnsupportedArithmeticCommand.into(),
861 location: Location { code, range: 0..2 },
862 };
863
864 let report = Report::from(&error);
865
866 assert_eq!(report.footnotes.len(), 1);
867 assert_eq!(report.footnotes[0].r#type, FootnoteType::Note);
868 assert_eq!(
869 report.footnotes[0].label,
870 "this error is reported because the `portable` shell option is enabled"
871 );
872 }
873}