yash_syntax/parser/
error.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2020 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Definition of errors that happen in the parser
18
19use crate::source::Location;
20#[allow(deprecated)]
21use crate::source::pretty::{Annotation, AnnotationType, MessageBase};
22use crate::source::pretty::{
23    Footnote, FootnoteType, Report, ReportType, Snippet, Span, SpanRole, add_span,
24};
25use crate::syntax::AndOr;
26use std::borrow::Cow;
27use std::rc::Rc;
28use thiserror::Error;
29
30/// Types of syntax errors
31#[derive(Clone, Debug, Eq, Error, PartialEq)]
32#[error("{}", self.message())]
33#[non_exhaustive]
34pub enum SyntaxError {
35    /// A backslash is at the end of the input.
36    IncompleteEscape,
37    /// A backslash is not followed by a character that makes a valid escape.
38    InvalidEscape,
39    /// A `(` lacks a closing `)`.
40    UnclosedParen { opening_location: Location },
41    /// A single quotation lacks a closing `'`.
42    UnclosedSingleQuote { opening_location: Location },
43    /// A double quotation lacks a closing `"`.
44    UnclosedDoubleQuote { opening_location: Location },
45    /// A `$'` lacks a closing `'`.
46    UnclosedDollarSingleQuote { opening_location: Location },
47    /// A parameter expansion lacks a closing `}`.
48    UnclosedParam { opening_location: Location },
49    /// A parameter expansion lacks a name.
50    EmptyParam,
51    /// A parameter expansion has an invalid name.
52    InvalidParam,
53    /// A modifier does not have a valid form in a parameter expansion.
54    InvalidModifier,
55    /// A braced parameter expansion has both a prefix and suffix modifier.
56    MultipleModifier,
57    /// A command substitution started with `$(` but lacks a closing `)`.
58    UnclosedCommandSubstitution { opening_location: Location },
59    /// A command substitution started with `` ` `` but lacks a closing `` ` ``.
60    UnclosedBackquote { opening_location: Location },
61    /// An arithmetic expansion lacks a closing `))`.
62    UnclosedArith { opening_location: Location },
63    /// A command begins with an inappropriate keyword or operator token.
64    InvalidCommandToken,
65    /// A separator is missing between commands.
66    MissingSeparator,
67    /// The file descriptor specified for a redirection cannot be used.
68    FdOutOfRange,
69    /// An I/O location prefix attached to a redirection has an unsupported format.
70    InvalidIoLocation,
71    /// A redirection operator is missing its operand.
72    MissingRedirOperand,
73    /// A here-document operator is missing its delimiter token.
74    MissingHereDocDelimiter,
75    /// A here-document operator is missing its corresponding content.
76    MissingHereDocContent,
77    /// A here-document content is missing its delimiter.
78    UnclosedHereDocContent { redir_op_location: Location },
79    /// An array assignment started with `=(` but lacks a closing `)`.
80    UnclosedArrayValue { opening_location: Location },
81    /// A `}` appears without a matching `{`.
82    UnopenedGrouping,
83    /// A grouping is not closed.
84    UnclosedGrouping { opening_location: Location },
85    /// A grouping contains no commands.
86    EmptyGrouping,
87    /// A `)` appears without a matching `(`.
88    UnopenedSubshell,
89    /// A subshell is not closed.
90    UnclosedSubshell { opening_location: Location },
91    /// A subshell contains no commands.
92    EmptySubshell,
93    /// A `do` appears outside a loop.
94    UnopenedLoop,
95    /// A `done` appears outside a loop.
96    UnopenedDoClause,
97    /// A do clause is not closed.
98    UnclosedDoClause { opening_location: Location },
99    /// A do clause contains no commands.
100    EmptyDoClause,
101    /// The variable name is missing in a for loop.
102    MissingForName,
103    /// The variable name is not a valid word in a for loop.
104    InvalidForName,
105    /// A value is not a valid word in a for loop.
106    InvalidForValue,
107    /// A for loop is missing a do clause.
108    MissingForBody { opening_location: Location },
109    /// A while loop is missing a do clause.
110    UnclosedWhileClause { opening_location: Location },
111    /// A while loop's condition is empty.
112    EmptyWhileCondition,
113    /// An until loop is missing a do clause.
114    UnclosedUntilClause { opening_location: Location },
115    /// An until loop's condition is empty.
116    EmptyUntilCondition,
117    /// An if command is missing the then clause.
118    IfMissingThen { if_location: Location },
119    /// An if command's condition is empty.
120    EmptyIfCondition,
121    /// An if command's body is empty.
122    EmptyIfBody,
123    /// An elif clause is missing the then clause.
124    ElifMissingThen { elif_location: Location },
125    /// An elif clause's condition is empty.
126    EmptyElifCondition,
127    /// An elif clause's body is empty.
128    EmptyElifBody,
129    /// An else clause is empty.
130    EmptyElse,
131    /// An `elif`, `else`, `then`, or `fi` appears outside an if command.
132    UnopenedIf,
133    /// An if command is not closed.
134    UnclosedIf { opening_location: Location },
135    /// The case command is missing its subject.
136    MissingCaseSubject,
137    /// The subject of the case command is not a valid word.
138    InvalidCaseSubject,
139    /// The case command is missing `in` after the subject.
140    MissingIn { opening_location: Location },
141    /// The `)` is missing in a case item.
142    UnclosedPatternList,
143    /// The pattern is missing in a case item.
144    MissingPattern,
145    /// The pattern is not a valid word token.
146    InvalidPattern,
147    /// The first pattern of a case item is `esac`.
148    #[deprecated = "this error no longer occurs"]
149    EsacAsPattern,
150    /// An `esac` or `;;` appears outside a case command.
151    UnopenedCase,
152    /// A case command is not closed.
153    UnclosedCase { opening_location: Location },
154    /// The `(` is not followed by `)` in a function definition.
155    UnmatchedParenthesis,
156    /// The function body is missing in a function definition command.
157    MissingFunctionBody,
158    /// A function body is not a compound command.
159    InvalidFunctionBody,
160    /// The keyword `in` is used as a command name.
161    InAsCommandName,
162    /// A pipeline is missing after a `&&` or `||` token.
163    MissingPipeline(AndOr),
164    /// Two successive `!` tokens.
165    DoubleNegation,
166    /// A `|` token is followed by a `!`.
167    BangAfterBar,
168    /// A command is missing after a `!` token.
169    MissingCommandAfterBang,
170    /// A command is missing after a `|` token.
171    MissingCommandAfterBar,
172    /// There is a redundant token.
173    RedundantToken,
174    /// A control escape (`\c...`) is incomplete in a dollar-single-quoted string.
175    IncompleteControlEscape,
176    /// A control-backslash escape (`\c\\`) is incomplete in a dollar-single-quoted string.
177    IncompleteControlBackslashEscape,
178    /// A control escape (`\c...`) does not have a valid control character.
179    InvalidControlEscape,
180    /// An octal escape is out of range (greater than `\377`) in a dollar-single-quoted string.
181    OctalEscapeOutOfRange,
182    /// An hexadecimal escape (`\x...`) is incomplete in a dollar-single-quoted string.
183    IncompleteHexEscape,
184    /// A Unicode escape (`\u...`) is incomplete in a dollar-single-quoted string.
185    IncompleteShortUnicodeEscape,
186    /// A Unicode escape (`\U...`) is incomplete in a dollar-single-quoted string.
187    IncompleteLongUnicodeEscape,
188    /// A Unicode escape (`\u...` or `\U...`) is out of range in a dollar-single-quoted string.
189    UnicodeEscapeOutOfRange,
190    /// The unsupported version of function definition syntax is used.
191    UnsupportedFunctionDefinitionSyntax,
192    /// A `[[ ... ]]` command is used.
193    UnsupportedDoubleBracketCommand,
194    /// A process redirection (`>(...)` or `<(...)`) is used.
195    UnsupportedProcessRedirection,
196}
197
198impl SyntaxError {
199    /// Returns an error message describing the error.
200    #[must_use]
201    pub fn message(&self) -> &'static str {
202        use SyntaxError::*;
203        match self {
204            IncompleteEscape => "the backslash is escaping nothing",
205            InvalidEscape => "the backslash escape is invalid",
206            UnclosedParen { .. } => "the parenthesis is not closed",
207            UnclosedSingleQuote { .. } => "the single quote is not closed",
208            UnclosedDoubleQuote { .. } => "the double quote is not closed",
209            UnclosedDollarSingleQuote { .. } => "the dollar single quote is not closed",
210            UnclosedParam { .. } => "the parameter expansion is not closed",
211            EmptyParam => "the parameter name is missing",
212            InvalidParam => "the parameter name is invalid",
213            InvalidModifier => "the parameter expansion contains a malformed modifier",
214            MultipleModifier => "a suffix modifier cannot be used together with a prefix modifier",
215            UnclosedCommandSubstitution { .. } => "the command substitution is not closed",
216            UnclosedBackquote { .. } => "the backquote is not closed",
217            UnclosedArith { .. } => "the arithmetic expansion is not closed",
218            InvalidCommandToken => "the command starts with an inappropriate token",
219            MissingSeparator => "a separator is missing between the commands",
220            FdOutOfRange => "the file descriptor is too large",
221            InvalidIoLocation => "the I/O location prefix is not valid",
222            MissingRedirOperand => "the redirection operator is missing its operand",
223            MissingHereDocDelimiter => "the here-document operator is missing its delimiter",
224            MissingHereDocContent => "content of the here-document is missing",
225            UnclosedHereDocContent { .. } => {
226                "the delimiter to close the here-document content is missing"
227            }
228            UnclosedArrayValue { .. } => "the array assignment value is not closed",
229            UnopenedGrouping | UnopenedSubshell | UnopenedLoop | UnopenedDoClause | UnopenedIf
230            | UnopenedCase | InAsCommandName => "the compound command delimiter is unmatched",
231            UnclosedGrouping { .. } => "the grouping is not closed",
232            EmptyGrouping => "the grouping is missing its content",
233            UnclosedSubshell { .. } => "the subshell is not closed",
234            EmptySubshell => "the subshell is missing its content",
235            UnclosedDoClause { .. } => "the `do` clause is missing its closing `done`",
236            EmptyDoClause => "the `do` clause is missing its content",
237            MissingForName => "the variable name is missing in the `for` loop",
238            InvalidForName => "the variable name is invalid",
239            InvalidForValue => "the operator token is invalid in the word list of the `for` loop",
240            MissingForBody { .. } => "the `for` loop is missing its `do` clause",
241            UnclosedWhileClause { .. } => "the `while` loop is missing its `do` clause",
242            EmptyWhileCondition => "the `while` loop is missing its condition",
243            UnclosedUntilClause { .. } => "the `until` loop is missing its `do` clause",
244            EmptyUntilCondition => "the `until` loop is missing its condition",
245            IfMissingThen { .. } => "the `if` command is missing the `then` clause",
246            EmptyIfCondition => "the `if` command is missing its condition",
247            EmptyIfBody => "the `if` command is missing its body",
248            ElifMissingThen { .. } => "the `elif` clause is missing the `then` clause",
249            EmptyElifCondition => "the `elif` clause is missing its condition",
250            EmptyElifBody => "the `elif` clause is missing its body",
251            EmptyElse => "the `else` clause is missing its content",
252            UnclosedIf { .. } => "the `if` command is missing its closing `fi`",
253            MissingCaseSubject => "the subject is missing after `case`",
254            InvalidCaseSubject => "the `case` command subject is not a valid word",
255            MissingIn { .. } => "`in` is missing in the `case` command",
256            UnclosedPatternList => "the pattern list is not properly closed by a `)`",
257            MissingPattern => "a pattern is missing in the `case` command",
258            InvalidPattern => "the pattern is not a valid word token",
259            #[allow(deprecated)]
260            EsacAsPattern => "`esac` cannot be the first of a pattern list",
261            UnclosedCase { .. } => "the `case` command is missing its closing `esac`",
262            UnmatchedParenthesis => "`)` is missing after `(`",
263            MissingFunctionBody => "the function body is missing",
264            InvalidFunctionBody => "the function body must be a compound command",
265            MissingPipeline(AndOr::AndThen) => "a command is missing after `&&`",
266            MissingPipeline(AndOr::OrElse) => "a command is missing after `||`",
267            DoubleNegation => "`!` cannot be used twice in a row",
268            BangAfterBar => "`!` cannot be used in the middle of a pipeline",
269            MissingCommandAfterBang => "a command is missing after `!`",
270            MissingCommandAfterBar => "a command is missing after `|`",
271            RedundantToken => "there is a redundant token",
272            IncompleteControlEscape => "the control escape is incomplete",
273            IncompleteControlBackslashEscape => "the control-backslash escape is incomplete",
274            InvalidControlEscape => "the control escape is invalid",
275            OctalEscapeOutOfRange => "the octal escape is out of range",
276            IncompleteHexEscape => "the hexadecimal escape is incomplete",
277            IncompleteShortUnicodeEscape | IncompleteLongUnicodeEscape => {
278                "the Unicode escape is incomplete"
279            }
280            UnicodeEscapeOutOfRange => "the Unicode escape is out of range",
281            UnsupportedFunctionDefinitionSyntax
282            | UnsupportedDoubleBracketCommand
283            | UnsupportedProcessRedirection => "unsupported syntax",
284        }
285    }
286
287    /// Returns a label for annotating the error location.
288    #[must_use]
289    pub fn label(&self) -> &'static str {
290        use SyntaxError::*;
291        match self {
292            IncompleteEscape => "expected an escaped character after the backslash",
293            InvalidEscape => "invalid escape sequence",
294            UnclosedParen { .. }
295            | UnclosedCommandSubstitution { .. }
296            | UnclosedArrayValue { .. }
297            | UnclosedSubshell { .. }
298            | UnclosedPatternList
299            | UnmatchedParenthesis => "expected `)`",
300            EmptyGrouping
301            | EmptySubshell
302            | EmptyDoClause
303            | EmptyWhileCondition
304            | EmptyUntilCondition
305            | EmptyIfCondition
306            | EmptyIfBody
307            | EmptyElifCondition
308            | EmptyElifBody
309            | EmptyElse
310            | MissingPipeline(_)
311            | MissingCommandAfterBang
312            | MissingCommandAfterBar => "expected a command",
313            InvalidForValue | MissingCaseSubject | InvalidCaseSubject | MissingPattern
314            | InvalidPattern => "expected a word",
315            UnclosedSingleQuote { .. } | UnclosedDollarSingleQuote { .. } => "expected `'`",
316            UnclosedDoubleQuote { .. } => "expected `\"`",
317            UnclosedParam { .. } | UnclosedGrouping { .. } => "expected `}`",
318            EmptyParam => "expected a parameter name",
319            InvalidParam => "not a valid named or positional parameter",
320            InvalidModifier => "broken modifier",
321            MultipleModifier => "conflicting modifier",
322            UnclosedBackquote { .. } => "expected '`'",
323            UnclosedArith { .. } => "expected `))`",
324            InvalidCommandToken => "does not begin a valid command",
325            MissingSeparator => "expected `;` or `&` before this token",
326            FdOutOfRange => "unsupported file descriptor",
327            InvalidIoLocation => "unsupported I/O location prefix",
328            MissingRedirOperand => "expected a redirection operand",
329            MissingHereDocDelimiter => "expected a delimiter word",
330            MissingHereDocContent => "content not found",
331            UnclosedHereDocContent { .. } => "missing delimiter",
332            UnopenedGrouping => "no grouping command to close",
333            UnopenedSubshell => "no subshell to close",
334            UnopenedLoop => "not in a loop",
335            UnopenedDoClause => "no `do` clause to close",
336            UnclosedDoClause { .. } => "expected `done`",
337            MissingForName => "expected a variable name",
338            InvalidForName => "not a valid variable name",
339            MissingForBody { .. } | UnclosedWhileClause { .. } | UnclosedUntilClause { .. } => {
340                "expected `do ... done`"
341            }
342            IfMissingThen { .. } | ElifMissingThen { .. } => "expected `then ... fi`",
343            UnopenedIf => "not in an `if` command",
344            UnclosedIf { .. } => "expected `fi`",
345            MissingIn { .. } => "expected `in`",
346            #[allow(deprecated)]
347            EsacAsPattern => "needs quoting",
348            UnopenedCase => "not in a `case` command",
349            UnclosedCase { .. } => "expected `esac`",
350            MissingFunctionBody | InvalidFunctionBody => "expected a compound command",
351            InAsCommandName => "cannot be used as a command name",
352            DoubleNegation => "only one `!` allowed",
353            BangAfterBar => "`!` not allowed here",
354            RedundantToken => "unexpected token",
355            IncompleteControlEscape => r"expected a control character after `\c`",
356            IncompleteControlBackslashEscape => r"expected another backslash after `\c\`",
357            InvalidControlEscape => "not a valid control character",
358            OctalEscapeOutOfRange => r"expected a value between \0 and \377",
359            IncompleteHexEscape => r"expected a hexadecimal digit after `\x`",
360            IncompleteShortUnicodeEscape => r"expected a hexadecimal digit after `\u`",
361            IncompleteLongUnicodeEscape => r"expected a hexadecimal digit after `\U`",
362            UnicodeEscapeOutOfRange => "not a valid Unicode scalar value",
363            UnsupportedFunctionDefinitionSyntax => "the `function` keyword is not yet supported",
364            UnsupportedDoubleBracketCommand => "the `[[ ... ]]` command is not yet supported",
365            UnsupportedProcessRedirection => "process redirection is not yet supported",
366        }
367    }
368
369    /// Returns a location related with the error cause and a message describing
370    /// the location.
371    #[must_use]
372    pub fn related_location(&self) -> Option<(&Location, &'static str)> {
373        use SyntaxError::*;
374        match self {
375            UnclosedParen { opening_location }
376            | UnclosedSubshell { opening_location }
377            | UnclosedArrayValue { opening_location } => {
378                Some((opening_location, "the opening parenthesis was here"))
379            }
380            UnclosedSingleQuote { opening_location }
381            | UnclosedDoubleQuote { opening_location }
382            | UnclosedDollarSingleQuote { opening_location } => {
383                Some((opening_location, "the opening quote was here"))
384            }
385            UnclosedParam { opening_location } => {
386                Some((opening_location, "the parameter started here"))
387            }
388            UnclosedCommandSubstitution { opening_location } => {
389                Some((opening_location, "the command substitution started here"))
390            }
391            UnclosedBackquote { opening_location } => {
392                Some((opening_location, "the opening backquote was here"))
393            }
394            UnclosedArith { opening_location } => {
395                Some((opening_location, "the arithmetic expansion started here"))
396            }
397            UnclosedHereDocContent { redir_op_location } => {
398                Some((redir_op_location, "the redirection operator was here"))
399            }
400            UnclosedGrouping { opening_location } => {
401                Some((opening_location, "the opening brace was here"))
402            }
403            UnclosedDoClause { opening_location } => {
404                Some((opening_location, "the `do` clause started here"))
405            }
406            MissingForBody { opening_location } => {
407                Some((opening_location, "the `for` loop started here"))
408            }
409            UnclosedWhileClause { opening_location } => {
410                Some((opening_location, "the `while` loop started here"))
411            }
412            UnclosedUntilClause { opening_location } => {
413                Some((opening_location, "the `until` loop started here"))
414            }
415            IfMissingThen { if_location }
416            | UnclosedIf {
417                opening_location: if_location,
418            } => Some((if_location, "the `if` command started here")),
419            ElifMissingThen { elif_location } => {
420                Some((elif_location, "the `elif` clause started here"))
421            }
422            MissingIn { opening_location } | UnclosedCase { opening_location } => {
423                Some((opening_location, "the `case` command started here"))
424            }
425            _ => None,
426        }
427    }
428}
429
430/// Types of errors that may happen in parsing
431#[derive(Clone, Debug, Error)]
432#[error("{}", self.message())]
433pub enum ErrorCause {
434    /// Error in an underlying input function
435    Io(#[from] Rc<std::io::Error>),
436    /// Syntax error
437    Syntax(#[from] SyntaxError),
438}
439
440impl PartialEq for ErrorCause {
441    fn eq(&self, other: &Self) -> bool {
442        match (self, other) {
443            (ErrorCause::Syntax(e1), ErrorCause::Syntax(e2)) => e1 == e2,
444            _ => false,
445        }
446    }
447}
448
449impl ErrorCause {
450    /// Returns an error message describing the error cause.
451    #[must_use]
452    pub fn message(&self) -> Cow<'static, str> {
453        use ErrorCause::*;
454        match self {
455            Io(e) => format!("cannot read commands: {e}").into(),
456            Syntax(e) => e.message().into(),
457        }
458    }
459
460    /// Returns a label for annotating the error location.
461    #[must_use]
462    pub fn label(&self) -> &'static str {
463        use ErrorCause::*;
464        match self {
465            Io(_) => "the command could be read up to here",
466            Syntax(e) => e.label(),
467        }
468    }
469
470    /// Returns a location related with the error cause and a message describing
471    /// the location.
472    #[must_use]
473    pub fn related_location(&self) -> Option<(&Location, &'static str)> {
474        use ErrorCause::*;
475        match self {
476            Io(_) => None,
477            Syntax(e) => e.related_location(),
478        }
479    }
480}
481
482impl From<std::io::Error> for ErrorCause {
483    fn from(e: std::io::Error) -> ErrorCause {
484        ErrorCause::from(Rc::new(e))
485    }
486}
487
488/// Explanation of a failure in parsing
489#[derive(Clone, Debug, Error, PartialEq)]
490#[error("{cause}")]
491pub struct Error {
492    pub cause: ErrorCause,
493    pub location: Location,
494}
495
496impl Error {
497    /// Returns a report for the error.
498    #[must_use]
499    pub fn to_report(&self) -> Report<'_> {
500        let mut report = Report::new();
501        report.r#type = ReportType::Error;
502        report.title = self.cause.message();
503        report.snippets = Snippet::with_primary_span(&self.location, self.cause.label().into());
504
505        if let Some((location, label)) = self.cause.related_location() {
506            let label = label.into();
507            let span = Span {
508                range: location.byte_range(),
509                role: SpanRole::Supplementary { label },
510            };
511            add_span(&location.code, span, &mut report.snippets);
512        }
513
514        if let ErrorCause::Syntax(SyntaxError::BangAfterBar) = &self.cause {
515            // TODO Suggest the change with SpanRole::Patch
516            report.footnotes.push(Footnote {
517                r#type: FootnoteType::Suggestion,
518                label: "surround the pipeline component in a grouping: `{ ! ...; }`".into(),
519            });
520        }
521
522        report
523    }
524}
525
526/// Converts the error into a report by calling [`Error::to_report`].
527impl<'a> From<&'a Error> for Report<'a> {
528    #[inline(always)]
529    fn from(error: &'a Error) -> Self {
530        error.to_report()
531    }
532}
533
534#[allow(deprecated)]
535impl MessageBase for Error {
536    fn message_title(&self) -> Cow<'_, str> {
537        self.cause.message()
538    }
539
540    fn main_annotation(&self) -> Annotation<'_> {
541        Annotation::new(
542            AnnotationType::Error,
543            self.cause.label().into(),
544            &self.location,
545        )
546    }
547
548    fn additional_annotations<'a, T: Extend<Annotation<'a>>>(&'a self, results: &mut T) {
549        // TODO Use Extend::extend_one
550        if let Some((location, label)) = self.cause.related_location() {
551            results.extend(std::iter::once(Annotation::new(
552                AnnotationType::Info,
553                label.into(),
554                location,
555            )));
556        }
557        if let ErrorCause::Syntax(SyntaxError::BangAfterBar) = &self.cause {
558            results.extend(std::iter::once(Annotation::new(
559                AnnotationType::Help,
560                "surround this in a grouping: `{ ! ...; }`".into(),
561                &self.location,
562            )));
563        }
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use crate::source::Code;
571    use crate::source::Source;
572    #[allow(deprecated)]
573    use crate::source::pretty::Message;
574    use assert_matches::assert_matches;
575    use std::num::NonZeroU64;
576    use std::rc::Rc;
577
578    #[test]
579    fn display_for_error() {
580        let code = Rc::new(Code {
581            value: "".to_string().into(),
582            start_line_number: NonZeroU64::new(1).unwrap(),
583            source: Source::Unknown.into(),
584        });
585        let location = Location { code, range: 0..42 };
586        let error = Error {
587            cause: SyntaxError::MissingHereDocDelimiter.into(),
588            location,
589        };
590        assert_eq!(
591            error.to_string(),
592            "the here-document operator is missing its delimiter"
593        );
594    }
595
596    #[test]
597    fn from_error_for_report() {
598        let code = Rc::new(Code {
599            value: "!!!".to_string().into(),
600            start_line_number: NonZeroU64::new(1).unwrap(),
601            source: Source::Unknown.into(),
602        });
603        let error = Error {
604            cause: SyntaxError::MissingHereDocDelimiter.into(),
605            location: Location { code, range: 0..42 },
606        };
607
608        let report = Report::from(&error);
609
610        assert_eq!(report.r#type, ReportType::Error);
611        assert_eq!(
612            report.title,
613            "the here-document operator is missing its delimiter"
614        );
615        assert_eq!(report.snippets.len(), 1);
616        assert_eq!(*report.snippets[0].code.value.borrow(), "!!!");
617        assert_eq!(report.snippets[0].spans.len(), 1);
618        assert_eq!(report.snippets[0].spans[0].range, 0..3);
619        assert_matches!(
620            &report.snippets[0].spans[0].role,
621            SpanRole::Primary { label } if label == "expected a delimiter word"
622        );
623        assert_eq!(report.footnotes, []);
624    }
625
626    #[allow(deprecated)]
627    #[test]
628    fn from_error_for_message() {
629        let code = Rc::new(Code {
630            value: "".to_string().into(),
631            start_line_number: NonZeroU64::new(1).unwrap(),
632            source: Source::Unknown.into(),
633        });
634        let location = Location { code, range: 0..42 };
635        let error = Error {
636            cause: SyntaxError::MissingHereDocDelimiter.into(),
637            location,
638        };
639        let message = Message::from(&error);
640        assert_eq!(message.r#type, AnnotationType::Error);
641        assert_eq!(
642            message.title,
643            "the here-document operator is missing its delimiter"
644        );
645        assert_eq!(message.annotations.len(), 1);
646        assert_eq!(message.annotations[0].r#type, AnnotationType::Error);
647        assert_eq!(message.annotations[0].label, "expected a delimiter word");
648        assert_eq!(message.annotations[0].location, &error.location);
649    }
650}