1use crate::source::Location;
20use crate::source::pretty::Annotation;
21use crate::source::pretty::AnnotationType;
22use crate::source::pretty::MessageBase;
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}
189
190impl SyntaxError {
191 #[must_use]
193 pub fn message(&self) -> &'static str {
194 use SyntaxError::*;
195 match self {
196 IncompleteEscape => "the backslash is escaping nothing",
197 InvalidEscape => "the backslash escape is invalid",
198 UnclosedParen { .. } => "the parenthesis is not closed",
199 UnclosedSingleQuote { .. } => "the single quote is not closed",
200 UnclosedDoubleQuote { .. } => "the double quote is not closed",
201 UnclosedDollarSingleQuote { .. } => "the dollar single quote is not closed",
202 UnclosedParam { .. } => "the parameter expansion is not closed",
203 EmptyParam => "the parameter name is missing",
204 InvalidParam => "the parameter name is invalid",
205 InvalidModifier => "the parameter expansion contains a malformed modifier",
206 MultipleModifier => "a suffix modifier cannot be used together with a prefix modifier",
207 UnclosedCommandSubstitution { .. } => "the command substitution is not closed",
208 UnclosedBackquote { .. } => "the backquote is not closed",
209 UnclosedArith { .. } => "the arithmetic expansion is not closed",
210 InvalidCommandToken => "the command starts with an inappropriate token",
211 MissingSeparator => "a separator is missing between the commands",
212 FdOutOfRange => "the file descriptor is too large",
213 InvalidIoLocation => "the I/O location prefix is not valid",
214 MissingRedirOperand => "the redirection operator is missing its operand",
215 MissingHereDocDelimiter => "the here-document operator is missing its delimiter",
216 MissingHereDocContent => "content of the here-document is missing",
217 UnclosedHereDocContent { .. } => {
218 "the delimiter to close the here-document content is missing"
219 }
220 UnclosedArrayValue { .. } => "the array assignment value is not closed",
221 UnopenedGrouping | UnopenedSubshell | UnopenedLoop | UnopenedDoClause | UnopenedIf
222 | UnopenedCase | InAsCommandName => "the compound command delimiter is unmatched",
223 UnclosedGrouping { .. } => "the grouping is not closed",
224 EmptyGrouping => "the grouping is missing its content",
225 UnclosedSubshell { .. } => "the subshell is not closed",
226 EmptySubshell => "the subshell is missing its content",
227 UnclosedDoClause { .. } => "the `do` clause is missing its closing `done`",
228 EmptyDoClause => "the `do` clause is missing its content",
229 MissingForName => "the variable name is missing in the `for` loop",
230 InvalidForName => "the variable name is invalid",
231 InvalidForValue => "the operator token is invalid in the word list of the `for` loop",
232 MissingForBody { .. } => "the `for` loop is missing its `do` clause",
233 UnclosedWhileClause { .. } => "the `while` loop is missing its `do` clause",
234 EmptyWhileCondition => "the `while` loop is missing its condition",
235 UnclosedUntilClause { .. } => "the `until` loop is missing its `do` clause",
236 EmptyUntilCondition => "the `until` loop is missing its condition",
237 IfMissingThen { .. } => "the `if` command is missing the `then` clause",
238 EmptyIfCondition => "the `if` command is missing its condition",
239 EmptyIfBody => "the `if` command is missing its body",
240 ElifMissingThen { .. } => "the `elif` clause is missing the `then` clause",
241 EmptyElifCondition => "the `elif` clause is missing its condition",
242 EmptyElifBody => "the `elif` clause is missing its body",
243 EmptyElse => "the `else` clause is missing its content",
244 UnclosedIf { .. } => "the `if` command is missing its closing `fi`",
245 MissingCaseSubject => "the subject is missing after `case`",
246 InvalidCaseSubject => "the `case` command subject is not a valid word",
247 MissingIn { .. } => "`in` is missing in the `case` command",
248 UnclosedPatternList => "the pattern list is not properly closed by a `)`",
249 MissingPattern => "a pattern is missing in the `case` command",
250 InvalidPattern => "the pattern is not a valid word token",
251 #[allow(deprecated)]
252 EsacAsPattern => "`esac` cannot be the first of a pattern list",
253 UnclosedCase { .. } => "the `case` command is missing its closing `esac`",
254 UnmatchedParenthesis => "`)` is missing after `(`",
255 MissingFunctionBody => "the function body is missing",
256 InvalidFunctionBody => "the function body must be a compound command",
257 MissingPipeline(AndOr::AndThen) => "a command is missing after `&&`",
258 MissingPipeline(AndOr::OrElse) => "a command is missing after `||`",
259 DoubleNegation => "`!` cannot be used twice in a row",
260 BangAfterBar => "`!` cannot be used in the middle of a pipeline",
261 MissingCommandAfterBang => "a command is missing after `!`",
262 MissingCommandAfterBar => "a command is missing after `|`",
263 RedundantToken => "there is a redundant token",
264 IncompleteControlEscape => "the control escape is incomplete",
265 IncompleteControlBackslashEscape => "the control-backslash escape is incomplete",
266 InvalidControlEscape => "the control escape is invalid",
267 OctalEscapeOutOfRange => "the octal escape is out of range",
268 IncompleteHexEscape => "the hexadecimal escape is incomplete",
269 IncompleteShortUnicodeEscape | IncompleteLongUnicodeEscape => {
270 "the Unicode escape is incomplete"
271 }
272 UnicodeEscapeOutOfRange => "the Unicode escape is out of range",
273 }
274 }
275
276 #[must_use]
278 pub fn label(&self) -> &'static str {
279 use SyntaxError::*;
280 match self {
281 IncompleteEscape => "expected an escaped character after the backslash",
282 InvalidEscape => "invalid escape sequence",
283 UnclosedParen { .. }
284 | UnclosedCommandSubstitution { .. }
285 | UnclosedArrayValue { .. }
286 | UnclosedSubshell { .. }
287 | UnclosedPatternList
288 | UnmatchedParenthesis => "expected `)`",
289 EmptyGrouping
290 | EmptySubshell
291 | EmptyDoClause
292 | EmptyWhileCondition
293 | EmptyUntilCondition
294 | EmptyIfCondition
295 | EmptyIfBody
296 | EmptyElifCondition
297 | EmptyElifBody
298 | EmptyElse
299 | MissingPipeline(_)
300 | MissingCommandAfterBang
301 | MissingCommandAfterBar => "expected a command",
302 InvalidForValue | MissingCaseSubject | InvalidCaseSubject | MissingPattern
303 | InvalidPattern => "expected a word",
304 UnclosedSingleQuote { .. } | UnclosedDollarSingleQuote { .. } => "expected `'`",
305 UnclosedDoubleQuote { .. } => "expected `\"`",
306 UnclosedParam { .. } | UnclosedGrouping { .. } => "expected `}`",
307 EmptyParam => "expected a parameter name",
308 InvalidParam => "not a valid named or positional parameter",
309 InvalidModifier => "broken modifier",
310 MultipleModifier => "conflicting modifier",
311 UnclosedBackquote { .. } => "expected '`'",
312 UnclosedArith { .. } => "expected `))`",
313 InvalidCommandToken => "does not begin a valid command",
314 MissingSeparator => "expected `;` or `&` before this token",
315 FdOutOfRange => "unsupported file descriptor",
316 InvalidIoLocation => "unsupported I/O location prefix",
317 MissingRedirOperand => "expected a redirection operand",
318 MissingHereDocDelimiter => "expected a delimiter word",
319 MissingHereDocContent => "content not found",
320 UnclosedHereDocContent { .. } => "missing delimiter",
321 UnopenedGrouping => "no grouping command to close",
322 UnopenedSubshell => "no subshell to close",
323 UnopenedLoop => "not in a loop",
324 UnopenedDoClause => "no `do` clause to close",
325 UnclosedDoClause { .. } => "expected `done`",
326 MissingForName => "expected a variable name",
327 InvalidForName => "not a valid variable name",
328 MissingForBody { .. } | UnclosedWhileClause { .. } | UnclosedUntilClause { .. } => {
329 "expected `do ... done`"
330 }
331 IfMissingThen { .. } | ElifMissingThen { .. } => "expected `then ... fi`",
332 UnopenedIf => "not in an `if` command",
333 UnclosedIf { .. } => "expected `fi`",
334 MissingIn { .. } => "expected `in`",
335 #[allow(deprecated)]
336 EsacAsPattern => "needs quoting",
337 UnopenedCase => "not in a `case` command",
338 UnclosedCase { .. } => "expected `esac`",
339 MissingFunctionBody | InvalidFunctionBody => "expected a compound command",
340 InAsCommandName => "cannot be used as a command name",
341 DoubleNegation => "only one `!` allowed",
342 BangAfterBar => "`!` not allowed here",
343 RedundantToken => "unexpected token",
344 IncompleteControlEscape => r"expected a control character after `\c`",
345 IncompleteControlBackslashEscape => r"expected another backslash after `\c\`",
346 InvalidControlEscape => "not a valid control character",
347 OctalEscapeOutOfRange => r"expected a value between \0 and \377",
348 IncompleteHexEscape => r"expected a hexadecimal digit after `\x`",
349 IncompleteShortUnicodeEscape => r"expected a hexadecimal digit after `\u`",
350 IncompleteLongUnicodeEscape => r"expected a hexadecimal digit after `\U`",
351 UnicodeEscapeOutOfRange => "not a valid Unicode scalar value",
352 }
353 }
354
355 #[must_use]
358 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
359 use SyntaxError::*;
360 match self {
361 UnclosedParen { opening_location }
362 | UnclosedSubshell { opening_location }
363 | UnclosedArrayValue { opening_location } => {
364 Some((opening_location, "the opening parenthesis was here"))
365 }
366 UnclosedSingleQuote { opening_location }
367 | UnclosedDoubleQuote { opening_location }
368 | UnclosedDollarSingleQuote { opening_location } => {
369 Some((opening_location, "the opening quote was here"))
370 }
371 UnclosedParam { opening_location } => {
372 Some((opening_location, "the parameter started here"))
373 }
374 UnclosedCommandSubstitution { opening_location } => {
375 Some((opening_location, "the command substitution started here"))
376 }
377 UnclosedBackquote { opening_location } => {
378 Some((opening_location, "the opening backquote was here"))
379 }
380 UnclosedArith { opening_location } => {
381 Some((opening_location, "the arithmetic expansion started here"))
382 }
383 UnclosedHereDocContent { redir_op_location } => {
384 Some((redir_op_location, "the redirection operator was here"))
385 }
386 UnclosedGrouping { opening_location } => {
387 Some((opening_location, "the opening brace was here"))
388 }
389 UnclosedDoClause { opening_location } => {
390 Some((opening_location, "the `do` clause started here"))
391 }
392 MissingForBody { opening_location } => {
393 Some((opening_location, "the `for` loop started here"))
394 }
395 UnclosedWhileClause { opening_location } => {
396 Some((opening_location, "the `while` loop started here"))
397 }
398 UnclosedUntilClause { opening_location } => {
399 Some((opening_location, "the `until` loop started here"))
400 }
401 IfMissingThen { if_location }
402 | UnclosedIf {
403 opening_location: if_location,
404 } => Some((if_location, "the `if` command started here")),
405 ElifMissingThen { elif_location } => {
406 Some((elif_location, "the `elif` clause started here"))
407 }
408 MissingIn { opening_location } | UnclosedCase { opening_location } => {
409 Some((opening_location, "the `case` command started here"))
410 }
411 _ => None,
412 }
413 }
414}
415
416#[derive(Clone, Debug, Error)]
418#[error("{}", self.message())]
419pub enum ErrorCause {
420 Io(#[from] Rc<std::io::Error>),
422 Syntax(#[from] SyntaxError),
424}
425
426impl PartialEq for ErrorCause {
427 fn eq(&self, other: &Self) -> bool {
428 match (self, other) {
429 (ErrorCause::Syntax(e1), ErrorCause::Syntax(e2)) => e1 == e2,
430 _ => false,
431 }
432 }
433}
434
435impl ErrorCause {
436 #[must_use]
438 pub fn message(&self) -> Cow<'static, str> {
439 use ErrorCause::*;
440 match self {
441 Io(e) => format!("cannot read commands: {e}").into(),
442 Syntax(e) => e.message().into(),
443 }
444 }
445
446 #[must_use]
448 pub fn label(&self) -> &'static str {
449 use ErrorCause::*;
450 match self {
451 Io(_) => "the command could be read up to here",
452 Syntax(e) => e.label(),
453 }
454 }
455
456 #[must_use]
459 pub fn related_location(&self) -> Option<(&Location, &'static str)> {
460 use ErrorCause::*;
461 match self {
462 Io(_) => None,
463 Syntax(e) => e.related_location(),
464 }
465 }
466}
467
468impl From<std::io::Error> for ErrorCause {
469 fn from(e: std::io::Error) -> ErrorCause {
470 ErrorCause::from(Rc::new(e))
471 }
472}
473
474#[derive(Clone, Debug, Error, PartialEq)]
476#[error("{cause}")]
477pub struct Error {
478 pub cause: ErrorCause,
479 pub location: Location,
480}
481
482impl MessageBase for Error {
483 fn message_title(&self) -> Cow<str> {
484 self.cause.message()
485 }
486
487 fn main_annotation(&self) -> Annotation {
488 Annotation::new(
489 AnnotationType::Error,
490 self.cause.label().into(),
491 &self.location,
492 )
493 }
494
495 fn additional_annotations<'a, T: Extend<Annotation<'a>>>(&'a self, results: &mut T) {
496 if let Some((location, label)) = self.cause.related_location() {
498 results.extend(std::iter::once(Annotation::new(
499 AnnotationType::Info,
500 label.into(),
501 location,
502 )));
503 }
504 if let ErrorCause::Syntax(SyntaxError::BangAfterBar) = &self.cause {
505 results.extend(std::iter::once(Annotation::new(
506 AnnotationType::Help,
507 "surround this in a grouping: `{ ! ...; }`".into(),
508 &self.location,
509 )));
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::source::Code;
518 use crate::source::Source;
519 use crate::source::pretty::Message;
520 use std::num::NonZeroU64;
521 use std::rc::Rc;
522
523 #[test]
524 fn display_for_error() {
525 let code = Rc::new(Code {
526 value: "".to_string().into(),
527 start_line_number: NonZeroU64::new(1).unwrap(),
528 source: Source::Unknown.into(),
529 });
530 let location = Location { code, range: 0..42 };
531 let error = Error {
532 cause: SyntaxError::MissingHereDocDelimiter.into(),
533 location,
534 };
535 assert_eq!(
536 error.to_string(),
537 "the here-document operator is missing its delimiter"
538 );
539 }
540
541 #[test]
542 fn from_error_for_message() {
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 let message = Message::from(&error);
554 assert_eq!(message.r#type, AnnotationType::Error);
555 assert_eq!(
556 message.title,
557 "the here-document operator is missing its delimiter"
558 );
559 assert_eq!(message.annotations.len(), 1);
560 assert_eq!(message.annotations[0].r#type, AnnotationType::Error);
561 assert_eq!(message.annotations[0].label, "expected a delimiter word");
562 assert_eq!(message.annotations[0].location, &error.location);
563 }
564}