Skip to main content

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 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/// 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(since = "0.12.1", note = "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    /// The keyword `]]` is used as a command name.
163    CloseBracketBracketAsCommandName,
164    /// A pipeline is missing after a `&&` or `||` token.
165    MissingPipeline(AndOr),
166    /// Two successive `!` tokens.
167    DoubleNegation,
168    /// A `|` token is followed by a `!`.
169    BangAfterBar,
170    /// A command is missing after a `!` token.
171    MissingCommandAfterBang,
172    /// A command is missing after a `|` token.
173    MissingCommandAfterBar,
174    /// There is a redundant token.
175    RedundantToken,
176    /// A control escape (`\c...`) is incomplete in a dollar-single-quoted string.
177    IncompleteControlEscape,
178    /// A control-backslash escape (`\c\\`) is incomplete in a dollar-single-quoted string.
179    IncompleteControlBackslashEscape,
180    /// A control escape (`\c...`) does not have a valid control character.
181    InvalidControlEscape,
182    /// An octal escape is out of range (greater than `\377`) in a dollar-single-quoted string.
183    OctalEscapeOutOfRange,
184    /// An hexadecimal escape (`\x...`) is incomplete in a dollar-single-quoted string.
185    IncompleteHexEscape,
186    /// A Unicode escape (`\u...`) is incomplete in a dollar-single-quoted string.
187    IncompleteShortUnicodeEscape,
188    /// A Unicode escape (`\U...`) is incomplete in a dollar-single-quoted string.
189    IncompleteLongUnicodeEscape,
190    /// A Unicode escape (`\u...` or `\U...`) is out of range in a dollar-single-quoted string.
191    UnicodeEscapeOutOfRange,
192    /// The unsupported version of function definition syntax is used.
193    UnsupportedFunctionDefinitionSyntax,
194    /// A `[[ ... ]]` command is used.
195    UnsupportedDoubleBracketCommand,
196    /// A `namespace` command is used.
197    UnsupportedNamespaceCommand,
198    /// A `select` command is used.
199    UnsupportedSelectCommand,
200    /// A process redirection (`>(...)` or `<(...)`) is used.
201    UnsupportedProcessRedirection,
202    /// A `((...))` arithmetic command is used at the beginning of a command
203    /// while the `portable` option is on.
204    ///
205    /// yash-rs parses `((` as nested subshells, but other shells parse it as an
206    /// arithmetic command, which yash-rs does not support. The `portable` option
207    /// rejects this ambiguous form; insert a space (`( (`) for nested subshells.
208    UnsupportedArithmeticCommand,
209    /// A `!(...)` extended glob is used at the beginning of a command while the
210    /// `portable` option is on.
211    ///
212    /// yash-rs parses `!(` as the `!` reserved word followed by a subshell, but
213    /// other shells parse it as an extended glob, which yash-rs does not support.
214    /// The `portable` option rejects this ambiguous form; insert a space (`! (`).
215    UnsupportedExtendedGlob,
216    /// A `;;&` or `;|` case terminator is used while the `portable` option is on.
217    ///
218    /// The operator is the offending terminator (`SemicolonSemicolonAnd` or
219    /// `SemicolonBar`).
220    NonPortableCaseTerminator(Operator),
221    /// A non-portable redirection operator (`>>|` or `<<<`) is used while the
222    /// `portable` option is on.
223    ///
224    /// The operator is the offending redirection operator (`Pipe` or `String`).
225    NonPortableRedirOperator(RedirOp),
226    /// An `IO_NUMBER` or `IO_LOCATION` token appears as a redirection operand
227    /// while the `portable` option is on.
228    ///
229    /// This happens when a token that should be a redirection operand is
230    /// immediately followed by a redirection operator without a separating
231    /// space, so yash-rs lexes it as an `IO_NUMBER` or `IO_LOCATION` token (as
232    /// in the `1` in `< 1>file`). POSIX does not recognize such a token as a
233    /// redirection operand, so this form is not portable.
234    IoTokenAsRedirOperand,
235    /// A reserved word follows a subshell or a redirection without a separator
236    /// while the `portable` option is on (as in `{ ( : ) }` or
237    /// `for i in 1; do ( : ) done`).
238    ///
239    /// POSIX recognizes a reserved word only when it is the first word of a
240    /// command or follows another reserved word. A subshell ends with the `)`
241    /// operator and a redirection ends with a word, so a clause-delimiting
242    /// reserved word (such as `}`, `done`, or `fi`) that immediately follows one
243    /// is not portably recognized.
244    MissingSeparatorBeforeReservedWord,
245    /// A command name ends with a `:` while the `portable` option is on.
246    ///
247    /// POSIX reserves words whose final character is a `:` for possible future
248    /// use, so such a word produces unspecified results when used where a
249    /// reserved word would be recognized (such as a command name). The lone
250    /// `:` (the colon built-in) is not affected.
251    ColonSuffixedCommandName,
252    /// A non-portable escape sequence is used in a dollar-single-quoted string
253    /// while the `portable` option is on.
254    ///
255    /// POSIX specifies a limited set of escape sequences for `$'...'`. This is
256    /// raised for yash extensions such as `\E`, `\?`, `\u`, `\U`, and `\c@`.
257    NonPortableEscape,
258    /// A `\x` escape in a dollar-single-quoted string is followed by more than
259    /// two hexadecimal digits while the `portable` option is on.
260    ///
261    /// POSIX leaves the result unspecified if more than two hexadecimal digits
262    /// follow `\x`, so such an escape is not portable.
263    TooLongHexEscape,
264    /// A `for` loop variable name is not a portable name while the `portable`
265    /// option is on.
266    ///
267    /// POSIX requires the name to be an unquoted `NAME` token consisting
268    /// solely of underscores, digits, and alphabetics from the portable
269    /// character set, not starting with a digit. This is raised when the name
270    /// is quoted, contains an expansion, or otherwise does not meet this
271    /// requirement.
272    NonPortableForName,
273    /// A function name is not a portable name while the `portable` option is
274    /// on.
275    ///
276    /// POSIX requires the name to be an unquoted `NAME` token consisting
277    /// solely of underscores, digits, and alphabetics from the portable
278    /// character set, not starting with a digit. This is raised when the name
279    /// is quoted, contains an expansion, or otherwise does not meet this
280    /// requirement.
281    NonPortableFunctionName,
282    /// An assignment name is not a portable name while the `portable` option
283    /// is on.
284    ///
285    /// A portable name consists solely of underscores, digits, and
286    /// alphabetics from the portable character set, not starting with a
287    /// digit. This is raised when the assignment name does not meet this
288    /// form, since other POSIX-conforming shells may not support it.
289    NonPortableAssignmentName,
290    /// An array assignment (`name=(...)`) is used while the `portable` option
291    /// is on.
292    ///
293    /// Array assignment is a yash extension that POSIX does not specify, so
294    /// other POSIX-conforming shells may not support it.
295    ArrayAssignment,
296    /// A function name is the same as a special built-in utility name while
297    /// the `portable` option is on.
298    ///
299    /// POSIX does not allow a function to have the same name as a special
300    /// built-in utility. See
301    /// [`POSIX_SPECIAL_BUILTIN_NAMES`](yash_env::builtin::POSIX_SPECIAL_BUILTIN_NAMES)
302    /// for the list of names this applies to.
303    SpecialBuiltinFunctionName,
304    /// A parameter expansion combines a special parameter with a modifier whose
305    /// result POSIX leaves unspecified, while the `portable` option is on.
306    ///
307    /// POSIX leaves the result unspecified for a length or switch modifier
308    /// applied to the special parameter `*` or `@` (as in `${#*}` or
309    /// `${@:-x}`), and for a trim modifier applied to the special parameter
310    /// `#`, `*`, or `@` (as in `${#%x}` or `${*#x}`).
311    NonPortableParamModifier,
312}
313
314impl SyntaxError {
315    /// Returns an error message describing the error.
316    #[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    /// Returns a label for annotating the error location.
431    #[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    /// Returns footnotes that supplement the error message.
550    ///
551    /// Each item pairs a [`FootnoteType`] with its text. The footnotes are to
552    /// be rendered after the error's source code snippet. The slice is empty
553    /// for errors that need no footnote.
554    #[must_use]
555    pub fn footnotes(&self) -> &'static [(FootnoteType, &'static str)] {
556        use SyntaxError::*;
557        match self {
558            BangAfterBar => &[(
559                FootnoteType::Suggestion,
560                // TODO Replace with a span-attached patch (SpanRole::Patch)
561                "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    /// Returns a location related with the error cause and a message describing
586    /// the location.
587    #[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/// Types of errors that may happen in parsing
647#[derive(Clone, Debug, Error)]
648#[error("{}", self.message())]
649pub enum ErrorCause {
650    /// Error in an underlying input function
651    Io(#[from] Rc<std::io::Error>),
652    /// Syntax error
653    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    /// Returns an error message describing the error cause.
667    #[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    /// Returns a label for annotating the error location.
677    #[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    /// Returns footnotes that supplement the error message.
687    ///
688    /// See [`SyntaxError::footnotes`] for details.
689    #[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    /// Returns a location related with the error cause and a message describing
699    /// the location.
700    #[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/// Explanation of a failure in parsing
717#[derive(Clone, Debug, Error, PartialEq)]
718#[error("{cause}")]
719pub struct Error {
720    pub cause: ErrorCause,
721    pub location: Location,
722}
723
724impl Error {
725    /// Returns a report for the error.
726    ///
727    /// This method constructs a [`Report`] from the error's cause and location.
728    /// The result includes information obtained from [`ErrorCause::message`],
729    /// [`ErrorCause::label`], [`ErrorCause::footnotes`], and
730    /// [`ErrorCause::related_location`].
731    #[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
761/// Converts the error into a report by calling [`Error::to_report`].
762impl<'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        // Errors caused by the `portable` option have a note.
829        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        // A suggestion is offered for `!` in the middle of a pipeline.
839        assert_eq!(
840            SyntaxError::BangAfterBar.footnotes(),
841            [(
842                FootnoteType::Suggestion,
843                "surround the pipeline component in a grouping: `{ ! ...; }`"
844            )]
845        );
846        // Errors unrelated to the `portable` option have no footnote.
847        assert_eq!(SyntaxError::MissingHereDocDelimiter.footnotes(), []);
848        // Unconditionally unsupported syntax is not caused by the option.
849        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}