use crate::source::Location;
use crate::source::pretty::{
Footnote, FootnoteType, Report, ReportType, Snippet, Span, SpanRole, add_span,
};
use crate::syntax::AndOr;
use std::borrow::Cow;
use std::rc::Rc;
use thiserror::Error;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("{}", self.message())]
#[non_exhaustive]
pub enum SyntaxError {
IncompleteEscape,
InvalidEscape,
UnclosedParen { opening_location: Location },
UnclosedSingleQuote { opening_location: Location },
UnclosedDoubleQuote { opening_location: Location },
UnclosedDollarSingleQuote { opening_location: Location },
UnclosedParam { opening_location: Location },
EmptyParam,
InvalidParam,
InvalidModifier,
MultipleModifier,
UnclosedCommandSubstitution { opening_location: Location },
UnclosedBackquote { opening_location: Location },
UnclosedArith { opening_location: Location },
InvalidCommandToken,
MissingSeparator,
FdOutOfRange,
InvalidIoLocation,
MissingRedirOperand,
MissingHereDocDelimiter,
MissingHereDocContent,
UnclosedHereDocContent { redir_op_location: Location },
UnclosedArrayValue { opening_location: Location },
UnopenedGrouping,
UnclosedGrouping { opening_location: Location },
EmptyGrouping,
UnopenedSubshell,
UnclosedSubshell { opening_location: Location },
EmptySubshell,
UnopenedLoop,
UnopenedDoClause,
UnclosedDoClause { opening_location: Location },
EmptyDoClause,
MissingForName,
InvalidForName,
InvalidForValue,
MissingForBody { opening_location: Location },
UnclosedWhileClause { opening_location: Location },
EmptyWhileCondition,
UnclosedUntilClause { opening_location: Location },
EmptyUntilCondition,
IfMissingThen { if_location: Location },
EmptyIfCondition,
EmptyIfBody,
ElifMissingThen { elif_location: Location },
EmptyElifCondition,
EmptyElifBody,
EmptyElse,
UnopenedIf,
UnclosedIf { opening_location: Location },
MissingCaseSubject,
InvalidCaseSubject,
MissingIn { opening_location: Location },
UnclosedPatternList,
MissingPattern,
InvalidPattern,
#[deprecated = "this error no longer occurs"]
EsacAsPattern,
UnopenedCase,
UnclosedCase { opening_location: Location },
UnmatchedParenthesis,
MissingFunctionBody,
InvalidFunctionBody,
InAsCommandName,
MissingPipeline(AndOr),
DoubleNegation,
BangAfterBar,
MissingCommandAfterBang,
MissingCommandAfterBar,
RedundantToken,
IncompleteControlEscape,
IncompleteControlBackslashEscape,
InvalidControlEscape,
OctalEscapeOutOfRange,
IncompleteHexEscape,
IncompleteShortUnicodeEscape,
IncompleteLongUnicodeEscape,
UnicodeEscapeOutOfRange,
UnsupportedFunctionDefinitionSyntax,
UnsupportedDoubleBracketCommand,
UnsupportedProcessRedirection,
}
impl SyntaxError {
#[must_use]
pub fn message(&self) -> &'static str {
use SyntaxError::*;
match self {
IncompleteEscape => "the backslash is escaping nothing",
InvalidEscape => "the backslash escape is invalid",
UnclosedParen { .. } => "the parenthesis is not closed",
UnclosedSingleQuote { .. } => "the single quote is not closed",
UnclosedDoubleQuote { .. } => "the double quote is not closed",
UnclosedDollarSingleQuote { .. } => "the dollar single quote is not closed",
UnclosedParam { .. } => "the parameter expansion is not closed",
EmptyParam => "the parameter name is missing",
InvalidParam => "the parameter name is invalid",
InvalidModifier => "the parameter expansion contains a malformed modifier",
MultipleModifier => "a suffix modifier cannot be used together with a prefix modifier",
UnclosedCommandSubstitution { .. } => "the command substitution is not closed",
UnclosedBackquote { .. } => "the backquote is not closed",
UnclosedArith { .. } => "the arithmetic expansion is not closed",
InvalidCommandToken => "the command starts with an inappropriate token",
MissingSeparator => "a separator is missing between the commands",
FdOutOfRange => "the file descriptor is too large",
InvalidIoLocation => "the I/O location prefix is not valid",
MissingRedirOperand => "the redirection operator is missing its operand",
MissingHereDocDelimiter => "the here-document operator is missing its delimiter",
MissingHereDocContent => "content of the here-document is missing",
UnclosedHereDocContent { .. } => {
"the delimiter to close the here-document content is missing"
}
UnclosedArrayValue { .. } => "the array assignment value is not closed",
UnopenedGrouping | UnopenedSubshell | UnopenedLoop | UnopenedDoClause | UnopenedIf
| UnopenedCase | InAsCommandName => "the compound command delimiter is unmatched",
UnclosedGrouping { .. } => "the grouping is not closed",
EmptyGrouping => "the grouping is missing its content",
UnclosedSubshell { .. } => "the subshell is not closed",
EmptySubshell => "the subshell is missing its content",
UnclosedDoClause { .. } => "the `do` clause is missing its closing `done`",
EmptyDoClause => "the `do` clause is missing its content",
MissingForName => "the variable name is missing in the `for` loop",
InvalidForName => "the variable name is invalid",
InvalidForValue => "the operator token is invalid in the word list of the `for` loop",
MissingForBody { .. } => "the `for` loop is missing its `do` clause",
UnclosedWhileClause { .. } => "the `while` loop is missing its `do` clause",
EmptyWhileCondition => "the `while` loop is missing its condition",
UnclosedUntilClause { .. } => "the `until` loop is missing its `do` clause",
EmptyUntilCondition => "the `until` loop is missing its condition",
IfMissingThen { .. } => "the `if` command is missing the `then` clause",
EmptyIfCondition => "the `if` command is missing its condition",
EmptyIfBody => "the `if` command is missing its body",
ElifMissingThen { .. } => "the `elif` clause is missing the `then` clause",
EmptyElifCondition => "the `elif` clause is missing its condition",
EmptyElifBody => "the `elif` clause is missing its body",
EmptyElse => "the `else` clause is missing its content",
UnclosedIf { .. } => "the `if` command is missing its closing `fi`",
MissingCaseSubject => "the subject is missing after `case`",
InvalidCaseSubject => "the `case` command subject is not a valid word",
MissingIn { .. } => "`in` is missing in the `case` command",
UnclosedPatternList => "the pattern list is not properly closed by a `)`",
MissingPattern => "a pattern is missing in the `case` command",
InvalidPattern => "the pattern is not a valid word token",
#[allow(deprecated)]
EsacAsPattern => "`esac` cannot be the first of a pattern list",
UnclosedCase { .. } => "the `case` command is missing its closing `esac`",
UnmatchedParenthesis => "`)` is missing after `(`",
MissingFunctionBody => "the function body is missing",
InvalidFunctionBody => "the function body must be a compound command",
MissingPipeline(AndOr::AndThen) => "a command is missing after `&&`",
MissingPipeline(AndOr::OrElse) => "a command is missing after `||`",
DoubleNegation => "`!` cannot be used twice in a row",
BangAfterBar => "`!` cannot be used in the middle of a pipeline",
MissingCommandAfterBang => "a command is missing after `!`",
MissingCommandAfterBar => "a command is missing after `|`",
RedundantToken => "there is a redundant token",
IncompleteControlEscape => "the control escape is incomplete",
IncompleteControlBackslashEscape => "the control-backslash escape is incomplete",
InvalidControlEscape => "the control escape is invalid",
OctalEscapeOutOfRange => "the octal escape is out of range",
IncompleteHexEscape => "the hexadecimal escape is incomplete",
IncompleteShortUnicodeEscape | IncompleteLongUnicodeEscape => {
"the Unicode escape is incomplete"
}
UnicodeEscapeOutOfRange => "the Unicode escape is out of range",
UnsupportedFunctionDefinitionSyntax
| UnsupportedDoubleBracketCommand
| UnsupportedProcessRedirection => "unsupported syntax",
}
}
#[must_use]
pub fn label(&self) -> &'static str {
use SyntaxError::*;
match self {
IncompleteEscape => "expected an escaped character after the backslash",
InvalidEscape => "invalid escape sequence",
UnclosedParen { .. }
| UnclosedCommandSubstitution { .. }
| UnclosedArrayValue { .. }
| UnclosedSubshell { .. }
| UnclosedPatternList
| UnmatchedParenthesis => "expected `)`",
EmptyGrouping
| EmptySubshell
| EmptyDoClause
| EmptyWhileCondition
| EmptyUntilCondition
| EmptyIfCondition
| EmptyIfBody
| EmptyElifCondition
| EmptyElifBody
| EmptyElse
| MissingPipeline(_)
| MissingCommandAfterBang
| MissingCommandAfterBar => "expected a command",
InvalidForValue | MissingCaseSubject | InvalidCaseSubject | MissingPattern
| InvalidPattern => "expected a word",
UnclosedSingleQuote { .. } | UnclosedDollarSingleQuote { .. } => "expected `'`",
UnclosedDoubleQuote { .. } => "expected `\"`",
UnclosedParam { .. } | UnclosedGrouping { .. } => "expected `}`",
EmptyParam => "expected a parameter name",
InvalidParam => "not a valid named or positional parameter",
InvalidModifier => "broken modifier",
MultipleModifier => "conflicting modifier",
UnclosedBackquote { .. } => "expected '`'",
UnclosedArith { .. } => "expected `))`",
InvalidCommandToken => "does not begin a valid command",
MissingSeparator => "expected `;` or `&` before this token",
FdOutOfRange => "unsupported file descriptor",
InvalidIoLocation => "unsupported I/O location prefix",
MissingRedirOperand => "expected a redirection operand",
MissingHereDocDelimiter => "expected a delimiter word",
MissingHereDocContent => "content not found",
UnclosedHereDocContent { .. } => "missing delimiter",
UnopenedGrouping => "no grouping command to close",
UnopenedSubshell => "no subshell to close",
UnopenedLoop => "not in a loop",
UnopenedDoClause => "no `do` clause to close",
UnclosedDoClause { .. } => "expected `done`",
MissingForName => "expected a variable name",
InvalidForName => "not a valid variable name",
MissingForBody { .. } | UnclosedWhileClause { .. } | UnclosedUntilClause { .. } => {
"expected `do ... done`"
}
IfMissingThen { .. } | ElifMissingThen { .. } => "expected `then ... fi`",
UnopenedIf => "not in an `if` command",
UnclosedIf { .. } => "expected `fi`",
MissingIn { .. } => "expected `in`",
#[allow(deprecated)]
EsacAsPattern => "needs quoting",
UnopenedCase => "not in a `case` command",
UnclosedCase { .. } => "expected `esac`",
MissingFunctionBody | InvalidFunctionBody => "expected a compound command",
InAsCommandName => "cannot be used as a command name",
DoubleNegation => "only one `!` allowed",
BangAfterBar => "`!` not allowed here",
RedundantToken => "unexpected token",
IncompleteControlEscape => r"expected a control character after `\c`",
IncompleteControlBackslashEscape => r"expected another backslash after `\c\`",
InvalidControlEscape => "not a valid control character",
OctalEscapeOutOfRange => r"expected a value between \0 and \377",
IncompleteHexEscape => r"expected a hexadecimal digit after `\x`",
IncompleteShortUnicodeEscape => r"expected a hexadecimal digit after `\u`",
IncompleteLongUnicodeEscape => r"expected a hexadecimal digit after `\U`",
UnicodeEscapeOutOfRange => "not a valid Unicode scalar value",
UnsupportedFunctionDefinitionSyntax => "the `function` keyword is not yet supported",
UnsupportedDoubleBracketCommand => "the `[[ ... ]]` command is not yet supported",
UnsupportedProcessRedirection => "process redirection is not yet supported",
}
}
#[must_use]
pub fn related_location(&self) -> Option<(&Location, &'static str)> {
use SyntaxError::*;
match self {
UnclosedParen { opening_location }
| UnclosedSubshell { opening_location }
| UnclosedArrayValue { opening_location } => {
Some((opening_location, "the opening parenthesis was here"))
}
UnclosedSingleQuote { opening_location }
| UnclosedDoubleQuote { opening_location }
| UnclosedDollarSingleQuote { opening_location } => {
Some((opening_location, "the opening quote was here"))
}
UnclosedParam { opening_location } => {
Some((opening_location, "the parameter started here"))
}
UnclosedCommandSubstitution { opening_location } => {
Some((opening_location, "the command substitution started here"))
}
UnclosedBackquote { opening_location } => {
Some((opening_location, "the opening backquote was here"))
}
UnclosedArith { opening_location } => {
Some((opening_location, "the arithmetic expansion started here"))
}
UnclosedHereDocContent { redir_op_location } => {
Some((redir_op_location, "the redirection operator was here"))
}
UnclosedGrouping { opening_location } => {
Some((opening_location, "the opening brace was here"))
}
UnclosedDoClause { opening_location } => {
Some((opening_location, "the `do` clause started here"))
}
MissingForBody { opening_location } => {
Some((opening_location, "the `for` loop started here"))
}
UnclosedWhileClause { opening_location } => {
Some((opening_location, "the `while` loop started here"))
}
UnclosedUntilClause { opening_location } => {
Some((opening_location, "the `until` loop started here"))
}
IfMissingThen { if_location }
| UnclosedIf {
opening_location: if_location,
} => Some((if_location, "the `if` command started here")),
ElifMissingThen { elif_location } => {
Some((elif_location, "the `elif` clause started here"))
}
MissingIn { opening_location } | UnclosedCase { opening_location } => {
Some((opening_location, "the `case` command started here"))
}
_ => None,
}
}
}
#[derive(Clone, Debug, Error)]
#[error("{}", self.message())]
pub enum ErrorCause {
Io(#[from] Rc<std::io::Error>),
Syntax(#[from] SyntaxError),
}
impl PartialEq for ErrorCause {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ErrorCause::Syntax(e1), ErrorCause::Syntax(e2)) => e1 == e2,
_ => false,
}
}
}
impl ErrorCause {
#[must_use]
pub fn message(&self) -> Cow<'static, str> {
use ErrorCause::*;
match self {
Io(e) => format!("cannot read commands: {e}").into(),
Syntax(e) => e.message().into(),
}
}
#[must_use]
pub fn label(&self) -> &'static str {
use ErrorCause::*;
match self {
Io(_) => "the command could be read up to here",
Syntax(e) => e.label(),
}
}
#[must_use]
pub fn related_location(&self) -> Option<(&Location, &'static str)> {
use ErrorCause::*;
match self {
Io(_) => None,
Syntax(e) => e.related_location(),
}
}
}
impl From<std::io::Error> for ErrorCause {
fn from(e: std::io::Error) -> ErrorCause {
ErrorCause::from(Rc::new(e))
}
}
#[derive(Clone, Debug, Error, PartialEq)]
#[error("{cause}")]
pub struct Error {
pub cause: ErrorCause,
pub location: Location,
}
impl Error {
#[must_use]
pub fn to_report(&self) -> Report<'_> {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = self.cause.message();
report.snippets = Snippet::with_primary_span(&self.location, self.cause.label().into());
if let Some((location, label)) = self.cause.related_location() {
let label = label.into();
let span = Span {
range: location.byte_range(),
role: SpanRole::Supplementary { label },
};
add_span(&location.code, span, &mut report.snippets);
}
if let ErrorCause::Syntax(SyntaxError::BangAfterBar) = &self.cause {
report.footnotes.push(Footnote {
r#type: FootnoteType::Suggestion,
label: "surround the pipeline component in a grouping: `{ ! ...; }`".into(),
});
}
report
}
}
impl<'a> From<&'a Error> for Report<'a> {
#[inline(always)]
fn from(error: &'a Error) -> Self {
error.to_report()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::Code;
use crate::source::Source;
use assert_matches::assert_matches;
use std::num::NonZeroU64;
use std::rc::Rc;
#[test]
fn display_for_error() {
let code = Rc::new(Code {
value: "".to_string().into(),
start_line_number: NonZeroU64::new(1).unwrap(),
source: Source::Unknown.into(),
});
let location = Location { code, range: 0..42 };
let error = Error {
cause: SyntaxError::MissingHereDocDelimiter.into(),
location,
};
assert_eq!(
error.to_string(),
"the here-document operator is missing its delimiter"
);
}
#[test]
fn from_error_for_report() {
let code = Rc::new(Code {
value: "!!!".to_string().into(),
start_line_number: NonZeroU64::new(1).unwrap(),
source: Source::Unknown.into(),
});
let error = Error {
cause: SyntaxError::MissingHereDocDelimiter.into(),
location: Location { code, range: 0..42 },
};
let report = Report::from(&error);
assert_eq!(report.r#type, ReportType::Error);
assert_eq!(
report.title,
"the here-document operator is missing its delimiter"
);
assert_eq!(report.snippets.len(), 1);
assert_eq!(*report.snippets[0].code.value.borrow(), "!!!");
assert_eq!(report.snippets[0].spans.len(), 1);
assert_eq!(report.snippets[0].spans[0].range, 0..3);
assert_matches!(
&report.snippets[0].spans[0].role,
SpanRole::Primary { label } if label == "expected a delimiter word"
);
assert_eq!(report.footnotes, []);
}
}