1use crate::source::Location;
20use crate::source::pretty::{
21 Footnote, FootnoteType, Report, ReportType, Snippet, Span, SpanRole, add_span,
22};
23use crate::syntax::AndOr;
24use std::borrow::Cow;
25use std::rc::Rc;
26use thiserror::Error;
27
28#[derive(Clone, Debug, Eq, Error, PartialEq)]
30#[error("{}", self.message())]
31#[non_exhaustive]
32pub enum SyntaxError {
33 IncompleteEscape,
35 InvalidEscape,
37 UnclosedParen { opening_location: Location },
39 UnclosedSingleQuote { opening_location: Location },
41 UnclosedDoubleQuote { opening_location: Location },
43 UnclosedDollarSingleQuote { opening_location: Location },
45 UnclosedParam { opening_location: Location },
47 EmptyParam,
49 InvalidParam,
51 InvalidModifier,
53 MultipleModifier,
55 UnclosedCommandSubstitution { opening_location: Location },
57 UnclosedBackquote { opening_location: Location },
59 UnclosedArith { opening_location: Location },
61 InvalidCommandToken,
63 MissingSeparator,
65 FdOutOfRange,
67 InvalidIoLocation,
69 MissingRedirOperand,
71 MissingHereDocDelimiter,
73 MissingHereDocContent,
75 UnclosedHereDocContent { redir_op_location: Location },
77 UnclosedArrayValue { opening_location: Location },
79 UnopenedGrouping,
81 UnclosedGrouping { opening_location: Location },
83 EmptyGrouping,
85 UnopenedSubshell,
87 UnclosedSubshell { opening_location: Location },
89 EmptySubshell,
91 UnopenedLoop,
93 UnopenedDoClause,
95 UnclosedDoClause { opening_location: Location },
97 EmptyDoClause,
99 MissingForName,
101 InvalidForName,
103 InvalidForValue,
105 MissingForBody { opening_location: Location },
107 UnclosedWhileClause { opening_location: Location },
109 EmptyWhileCondition,
111 UnclosedUntilClause { opening_location: Location },
113 EmptyUntilCondition,
115 IfMissingThen { if_location: Location },
117 EmptyIfCondition,
119 EmptyIfBody,
121 ElifMissingThen { elif_location: Location },
123 EmptyElifCondition,
125 EmptyElifBody,
127 EmptyElse,
129 UnopenedIf,
131 UnclosedIf { opening_location: Location },
133 MissingCaseSubject,
135 InvalidCaseSubject,
137 MissingIn { opening_location: Location },
139 UnclosedPatternList,
141 MissingPattern,
143 InvalidPattern,
145 #[deprecated = "this error no longer occurs"]
147 EsacAsPattern,
148 UnopenedCase,
150 UnclosedCase { opening_location: Location },
152 UnmatchedParenthesis,
154 MissingFunctionBody,
156 InvalidFunctionBody,
158 InAsCommandName,
160 MissingPipeline(AndOr),
162 DoubleNegation,
164 BangAfterBar,
166 MissingCommandAfterBang,
168 MissingCommandAfterBar,
170 RedundantToken,
172 IncompleteControlEscape,
174 IncompleteControlBackslashEscape,
176 InvalidControlEscape,
178 OctalEscapeOutOfRange,
180 IncompleteHexEscape,
182 IncompleteShortUnicodeEscape,
184 IncompleteLongUnicodeEscape,
186 UnicodeEscapeOutOfRange,
188 UnsupportedFunctionDefinitionSyntax,
190 UnsupportedDoubleBracketCommand,
192 UnsupportedProcessRedirection,
194}
195
196impl SyntaxError {
197 #[must_use]
199 pub fn message(&self) -> &'static str {
200 use SyntaxError::*;
201 match self {
202 IncompleteEscape => "the backslash is escaping nothing",
203 InvalidEscape => "the backslash escape is invalid",
204 UnclosedParen { .. } => "the parenthesis is not closed",
205 UnclosedSingleQuote { .. } => "the single quote is not closed",
206 UnclosedDoubleQuote { .. } => "the double quote is not closed",
207 UnclosedDollarSingleQuote { .. } => "the dollar single quote is not closed",
208 UnclosedParam { .. } => "the parameter expansion is not closed",
209 EmptyParam => "the parameter name is missing",
210 InvalidParam => "the parameter name is invalid",
211 InvalidModifier => "the parameter expansion contains a malformed modifier",
212 MultipleModifier => "a suffix modifier cannot be used together with a prefix modifier",
213 UnclosedCommandSubstitution { .. } => "the command substitution is not closed",
214 UnclosedBackquote { .. } => "the backquote is not closed",
215 UnclosedArith { .. } => "the arithmetic expansion is not closed",
216 InvalidCommandToken => "the command starts with an inappropriate token",
217 MissingSeparator => "a separator is missing between the commands",
218 FdOutOfRange => "the file descriptor is too large",
219 InvalidIoLocation => "the I/O location prefix is not valid",
220 MissingRedirOperand => "the redirection operator is missing its operand",
221 MissingHereDocDelimiter => "the here-document operator is missing its delimiter",
222 MissingHereDocContent => "content of the here-document is missing",
223 UnclosedHereDocContent { .. } => {
224 "the delimiter to close the here-document content is missing"
225 }
226 UnclosedArrayValue { .. } => "the array assignment value is not closed",
227 UnopenedGrouping | UnopenedSubshell | UnopenedLoop | UnopenedDoClause | UnopenedIf
228 | UnopenedCase | InAsCommandName => "the compound command delimiter is unmatched",
229 UnclosedGrouping { .. } => "the grouping is not closed",
230 EmptyGrouping => "the grouping is missing its content",
231 UnclosedSubshell { .. } => "the subshell is not closed",
232 EmptySubshell => "the subshell is missing its content",
233 UnclosedDoClause { .. } => "the `do` clause is missing its closing `done`",
234 EmptyDoClause => "the `do` clause is missing its content",
235 MissingForName => "the variable name is missing in the `for` loop",
236 InvalidForName => "the variable name is invalid",
237 InvalidForValue => "the operator token is invalid in the word list of the `for` loop",
238 MissingForBody { .. } => "the `for` loop is missing its `do` clause",
239 UnclosedWhileClause { .. } => "the `while` loop is missing its `do` clause",
240 EmptyWhileCondition => "the `while` loop is missing its condition",
241 UnclosedUntilClause { .. } => "the `until` loop is missing its `do` clause",
242 EmptyUntilCondition => "the `until` loop is missing its condition",
243 IfMissingThen { .. } => "the `if` command is missing the `then` clause",
244 EmptyIfCondition => "the `if` command is missing its condition",
245 EmptyIfBody => "the `if` command is missing its body",
246 ElifMissingThen { .. } => "the `elif` clause is missing the `then` clause",
247 EmptyElifCondition => "the `elif` clause is missing its condition",
248 EmptyElifBody => "the `elif` clause is missing its body",
249 EmptyElse => "the `else` clause is missing its content",
250 UnclosedIf { .. } => "the `if` command is missing its closing `fi`",
251 MissingCaseSubject => "the subject is missing after `case`",
252 InvalidCaseSubject => "the `case` command subject is not a valid word",
253 MissingIn { .. } => "`in` is missing in the `case` command",
254 UnclosedPatternList => "the pattern list is not properly closed by a `)`",
255 MissingPattern => "a pattern is missing in the `case` command",
256 InvalidPattern => "the pattern is not a valid word token",
257 #[allow(deprecated)]
258 EsacAsPattern => "`esac` cannot be the first of a pattern list",
259 UnclosedCase { .. } => "the `case` command is missing its closing `esac`",
260 UnmatchedParenthesis => "`)` is missing after `(`",
261 MissingFunctionBody => "the function body is missing",
262 InvalidFunctionBody => "the function body must be a compound command",
263 MissingPipeline(AndOr::AndThen) => "a command is missing after `&&`",
264 MissingPipeline(AndOr::OrElse) => "a command is missing after `||`",
265 DoubleNegation => "`!` cannot be used twice in a row",
266 BangAfterBar => "`!` cannot be used in the middle of a pipeline",
267 MissingCommandAfterBang => "a command is missing after `!`",
268 MissingCommandAfterBar => "a command is missing after `|`",
269 RedundantToken => "there is a redundant token",
270 IncompleteControlEscape => "the control escape is incomplete",
271 IncompleteControlBackslashEscape => "the control-backslash escape is incomplete",
272 InvalidControlEscape => "the control escape is invalid",
273 OctalEscapeOutOfRange => "the octal escape is out of range",
274 IncompleteHexEscape => "the hexadecimal escape is incomplete",
275 IncompleteShortUnicodeEscape | IncompleteLongUnicodeEscape => {
276 "the Unicode escape is incomplete"
277 }
278 UnicodeEscapeOutOfRange => "the Unicode escape is out of range",
279 UnsupportedFunctionDefinitionSyntax
280 | UnsupportedDoubleBracketCommand
281 | UnsupportedProcessRedirection => "unsupported syntax",
282 }
283 }
284
285 #[must_use]
287 pub fn label(&self) -> &'static str {
288 use SyntaxError::*;
289 match self {
290 IncompleteEscape => "expected an escaped character after the backslash",
291 InvalidEscape => "invalid escape sequence",
292 UnclosedParen { .. }
293 | UnclosedCommandSubstitution { .. }
294 | UnclosedArrayValue { .. }
295 | UnclosedSubshell { .. }
296 | UnclosedPatternList
297 | UnmatchedParenthesis => "expected `)`",
298 EmptyGrouping
299 | EmptySubshell
300 | EmptyDoClause
301 | EmptyWhileCondition
302 | EmptyUntilCondition
303 | EmptyIfCondition
304 | EmptyIfBody
305 | EmptyElifCondition
306 | EmptyElifBody
307 | EmptyElse
308 | MissingPipeline(_)
309 | MissingCommandAfterBang
310 | MissingCommandAfterBar => "expected a command",
311 InvalidForValue | MissingCaseSubject | InvalidCaseSubject | MissingPattern
312 | InvalidPattern => "expected a word",
313 UnclosedSingleQuote { .. } | UnclosedDollarSingleQuote { .. } => "expected `'`",
314 UnclosedDoubleQuote { .. } => "expected `\"`",
315 UnclosedParam { .. } | UnclosedGrouping { .. } => "expected `}`",
316 EmptyParam => "expected a parameter name",
317 InvalidParam => "not a valid named or positional parameter",
318 InvalidModifier => "broken modifier",
319 MultipleModifier => "conflicting modifier",
320 UnclosedBackquote { .. } => "expected '`'",
321 UnclosedArith { .. } => "expected `))`",
322 InvalidCommandToken => "does not begin a valid command",
323 MissingSeparator => "expected `;` or `&` before this token",
324 FdOutOfRange => "unsupported file descriptor",
325 InvalidIoLocation => "unsupported I/O location prefix",
326 MissingRedirOperand => "expected a redirection operand",
327 MissingHereDocDelimiter => "expected a delimiter word",
328 MissingHereDocContent => "content not found",
329 UnclosedHereDocContent { .. } => "missing delimiter",
330 UnopenedGrouping => "no grouping command to close",
331 UnopenedSubshell => "no subshell to close",
332 UnopenedLoop => "not in a loop",
333 UnopenedDoClause => "no `do` clause to close",
334 UnclosedDoClause { .. } => "expected `done`",
335 MissingForName => "expected a variable name",
336 InvalidForName => "not a valid variable name",
337 MissingForBody { .. } | UnclosedWhileClause { .. } | UnclosedUntilClause { .. } => {
338 "expected `do ... done`"
339 }
340 IfMissingThen { .. } | ElifMissingThen { .. } => "expected `then ... fi`",
341 UnopenedIf => "not in an `if` command",
342 UnclosedIf { .. } => "expected `fi`",
343 MissingIn { .. } => "expected `in`",
344 #[allow(deprecated)]
345 EsacAsPattern => "needs quoting",
346 UnopenedCase => "not in a `case` command",
347 UnclosedCase { .. } => "expected `esac`",
348 MissingFunctionBody | InvalidFunctionBody => "expected a compound command",
349 InAsCommandName => "cannot be used as a command name",
350 DoubleNegation => "only one `!` allowed",
351 BangAfterBar => "`!` not allowed here",
352 RedundantToken => "unexpected token",
353 IncompleteControlEscape => r"expected a control character after `\c`",
354 IncompleteControlBackslashEscape => r"expected another backslash after `\c\`",
355 InvalidControlEscape => "not a valid control character",
356 OctalEscapeOutOfRange => r"expected a value between \0 and \377",
357 IncompleteHexEscape => r"expected a hexadecimal digit after `\x`",
358 IncompleteShortUnicodeEscape => r"expected a hexadecimal digit after `\u`",
359 IncompleteLongUnicodeEscape => r"expected a hexadecimal digit after `\U`",
360 UnicodeEscapeOutOfRange => "not a valid Unicode scalar value",
361 UnsupportedFunctionDefinitionSyntax => "the `function` keyword is not yet supported",
362 UnsupportedDoubleBracketCommand => "the `[[ ... ]]` command is not yet supported",
363 UnsupportedProcessRedirection => "process redirection is not yet supported",
364 }
365 }
366
367 #[must_use]
370 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
371 use SyntaxError::*;
372 match self {
373 UnclosedParen { opening_location }
374 | UnclosedSubshell { opening_location }
375 | UnclosedArrayValue { opening_location } => {
376 Some((opening_location, "the opening parenthesis was here"))
377 }
378 UnclosedSingleQuote { opening_location }
379 | UnclosedDoubleQuote { opening_location }
380 | UnclosedDollarSingleQuote { opening_location } => {
381 Some((opening_location, "the opening quote was here"))
382 }
383 UnclosedParam { opening_location } => {
384 Some((opening_location, "the parameter started here"))
385 }
386 UnclosedCommandSubstitution { opening_location } => {
387 Some((opening_location, "the command substitution started here"))
388 }
389 UnclosedBackquote { opening_location } => {
390 Some((opening_location, "the opening backquote was here"))
391 }
392 UnclosedArith { opening_location } => {
393 Some((opening_location, "the arithmetic expansion started here"))
394 }
395 UnclosedHereDocContent { redir_op_location } => {
396 Some((redir_op_location, "the redirection operator was here"))
397 }
398 UnclosedGrouping { opening_location } => {
399 Some((opening_location, "the opening brace was here"))
400 }
401 UnclosedDoClause { opening_location } => {
402 Some((opening_location, "the `do` clause started here"))
403 }
404 MissingForBody { opening_location } => {
405 Some((opening_location, "the `for` loop started here"))
406 }
407 UnclosedWhileClause { opening_location } => {
408 Some((opening_location, "the `while` loop started here"))
409 }
410 UnclosedUntilClause { opening_location } => {
411 Some((opening_location, "the `until` loop started here"))
412 }
413 IfMissingThen { if_location }
414 | UnclosedIf {
415 opening_location: if_location,
416 } => Some((if_location, "the `if` command started here")),
417 ElifMissingThen { elif_location } => {
418 Some((elif_location, "the `elif` clause started here"))
419 }
420 MissingIn { opening_location } | UnclosedCase { opening_location } => {
421 Some((opening_location, "the `case` command started here"))
422 }
423 _ => None,
424 }
425 }
426}
427
428#[derive(Clone, Debug, Error)]
430#[error("{}", self.message())]
431pub enum ErrorCause {
432 Io(#[from] Rc<std::io::Error>),
434 Syntax(#[from] SyntaxError),
436}
437
438impl PartialEq for ErrorCause {
439 fn eq(&self, other: &Self) -> bool {
440 match (self, other) {
441 (ErrorCause::Syntax(e1), ErrorCause::Syntax(e2)) => e1 == e2,
442 _ => false,
443 }
444 }
445}
446
447impl ErrorCause {
448 #[must_use]
450 pub fn message(&self) -> Cow<'static, str> {
451 use ErrorCause::*;
452 match self {
453 Io(e) => format!("cannot read commands: {e}").into(),
454 Syntax(e) => e.message().into(),
455 }
456 }
457
458 #[must_use]
460 pub fn label(&self) -> &'static str {
461 use ErrorCause::*;
462 match self {
463 Io(_) => "the command could be read up to here",
464 Syntax(e) => e.label(),
465 }
466 }
467
468 #[must_use]
471 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
472 use ErrorCause::*;
473 match self {
474 Io(_) => None,
475 Syntax(e) => e.related_location(),
476 }
477 }
478}
479
480impl From<std::io::Error> for ErrorCause {
481 fn from(e: std::io::Error) -> ErrorCause {
482 ErrorCause::from(Rc::new(e))
483 }
484}
485
486#[derive(Clone, Debug, Error, PartialEq)]
488#[error("{cause}")]
489pub struct Error {
490 pub cause: ErrorCause,
491 pub location: Location,
492}
493
494impl Error {
495 #[must_use]
497 pub fn to_report(&self) -> Report<'_> {
498 let mut report = Report::new();
499 report.r#type = ReportType::Error;
500 report.title = self.cause.message();
501 report.snippets = Snippet::with_primary_span(&self.location, self.cause.label().into());
502
503 if let Some((location, label)) = self.cause.related_location() {
504 let label = label.into();
505 let span = Span {
506 range: location.byte_range(),
507 role: SpanRole::Supplementary { label },
508 };
509 add_span(&location.code, span, &mut report.snippets);
510 }
511
512 if let ErrorCause::Syntax(SyntaxError::BangAfterBar) = &self.cause {
513 report.footnotes.push(Footnote {
515 r#type: FootnoteType::Suggestion,
516 label: "surround the pipeline component in a grouping: `{ ! ...; }`".into(),
517 });
518 }
519
520 report
521 }
522}
523
524impl<'a> From<&'a Error> for Report<'a> {
526 #[inline(always)]
527 fn from(error: &'a Error) -> Self {
528 error.to_report()
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::source::Code;
536 use crate::source::Source;
537 use assert_matches::assert_matches;
538 use std::num::NonZeroU64;
539 use std::rc::Rc;
540
541 #[test]
542 fn display_for_error() {
543 let code = Rc::new(Code {
544 value: "".to_string().into(),
545 start_line_number: NonZeroU64::new(1).unwrap(),
546 source: Source::Unknown.into(),
547 });
548 let location = Location { code, range: 0..42 };
549 let error = Error {
550 cause: SyntaxError::MissingHereDocDelimiter.into(),
551 location,
552 };
553 assert_eq!(
554 error.to_string(),
555 "the here-document operator is missing its delimiter"
556 );
557 }
558
559 #[test]
560 fn from_error_for_report() {
561 let code = Rc::new(Code {
562 value: "!!!".to_string().into(),
563 start_line_number: NonZeroU64::new(1).unwrap(),
564 source: Source::Unknown.into(),
565 });
566 let error = Error {
567 cause: SyntaxError::MissingHereDocDelimiter.into(),
568 location: Location { code, range: 0..42 },
569 };
570
571 let report = Report::from(&error);
572
573 assert_eq!(report.r#type, ReportType::Error);
574 assert_eq!(
575 report.title,
576 "the here-document operator is missing its delimiter"
577 );
578 assert_eq!(report.snippets.len(), 1);
579 assert_eq!(*report.snippets[0].code.value.borrow(), "!!!");
580 assert_eq!(report.snippets[0].spans.len(), 1);
581 assert_eq!(report.snippets[0].spans[0].range, 0..3);
582 assert_matches!(
583 &report.snippets[0].spans[0].role,
584 SpanRole::Primary { label } if label == "expected a delimiter word"
585 );
586 assert_eq!(report.footnotes, []);
587 }
588}