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 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 MessageBase for Error {
495 fn message_title(&self) -> Cow<'_, str> {
496 self.cause.message()
497 }
498
499 fn main_annotation(&self) -> Annotation<'_> {
500 Annotation::new(
501 AnnotationType::Error,
502 self.cause.label().into(),
503 &self.location,
504 )
505 }
506
507 fn additional_annotations<'a, T: Extend<Annotation<'a>>>(&'a self, results: &mut T) {
508 if let Some((location, label)) = self.cause.related_location() {
510 results.extend(std::iter::once(Annotation::new(
511 AnnotationType::Info,
512 label.into(),
513 location,
514 )));
515 }
516 if let ErrorCause::Syntax(SyntaxError::BangAfterBar) = &self.cause {
517 results.extend(std::iter::once(Annotation::new(
518 AnnotationType::Help,
519 "surround this in a grouping: `{ ! ...; }`".into(),
520 &self.location,
521 )));
522 }
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::source::Code;
530 use crate::source::Source;
531 use crate::source::pretty::Message;
532 use std::num::NonZeroU64;
533 use std::rc::Rc;
534
535 #[test]
536 fn display_for_error() {
537 let code = Rc::new(Code {
538 value: "".to_string().into(),
539 start_line_number: NonZeroU64::new(1).unwrap(),
540 source: Source::Unknown.into(),
541 });
542 let location = Location { code, range: 0..42 };
543 let error = Error {
544 cause: SyntaxError::MissingHereDocDelimiter.into(),
545 location,
546 };
547 assert_eq!(
548 error.to_string(),
549 "the here-document operator is missing its delimiter"
550 );
551 }
552
553 #[test]
554 fn from_error_for_message() {
555 let code = Rc::new(Code {
556 value: "".to_string().into(),
557 start_line_number: NonZeroU64::new(1).unwrap(),
558 source: Source::Unknown.into(),
559 });
560 let location = Location { code, range: 0..42 };
561 let error = Error {
562 cause: SyntaxError::MissingHereDocDelimiter.into(),
563 location,
564 };
565 let message = Message::from(&error);
566 assert_eq!(message.r#type, AnnotationType::Error);
567 assert_eq!(
568 message.title,
569 "the here-document operator is missing its delimiter"
570 );
571 assert_eq!(message.annotations.len(), 1);
572 assert_eq!(message.annotations[0].r#type, AnnotationType::Error);
573 assert_eq!(message.annotations[0].label, "expected a delimiter word");
574 assert_eq!(message.annotations[0].location, &error.location);
575 }
576}