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