1use 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#[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 = "this error no longer occurs"]
149 EsacAsPattern,
150 UnopenedCase,
152 UnclosedCase { opening_location: Location },
154 UnmatchedParenthesis,
156 MissingFunctionBody,
158 InvalidFunctionBody,
160 InAsCommandName,
162 MissingPipeline(AndOr),
164 DoubleNegation,
166 BangAfterBar,
168 MissingCommandAfterBang,
170 MissingCommandAfterBar,
172 RedundantToken,
174 IncompleteControlEscape,
176 IncompleteControlBackslashEscape,
178 InvalidControlEscape,
180 OctalEscapeOutOfRange,
182 IncompleteHexEscape,
184 IncompleteShortUnicodeEscape,
186 IncompleteLongUnicodeEscape,
188 UnicodeEscapeOutOfRange,
190 UnsupportedFunctionDefinitionSyntax,
192 UnsupportedDoubleBracketCommand,
194 UnsupportedProcessRedirection,
196}
197
198impl SyntaxError {
199 #[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 #[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 #[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#[derive(Clone, Debug, Error)]
432#[error("{}", self.message())]
433pub enum ErrorCause {
434 Io(#[from] Rc<std::io::Error>),
436 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 #[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 #[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 #[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#[derive(Clone, Debug, Error, PartialEq)]
490#[error("{cause}")]
491pub struct Error {
492 pub cause: ErrorCause,
493 pub location: Location,
494}
495
496impl Error {
497 #[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 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
526impl<'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 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}