mq-lang 0.6.0

Core language implementation for mq query language
Documentation
use smol_str::SmolStr;
use thiserror::Error;

use crate::{Token, module::ModuleId, selector};

/// Errors that occur during parsing of mq source code.
#[derive(Error, Debug, PartialEq)]
pub enum SyntaxError {
    /// An environment variable was not found.
    #[error("Undefined environment variable `{1}`")]
    EnvNotFound(Token, SmolStr),
    /// An unexpected token was encountered during parsing.
    #[error("Unexpected token `{}`", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
    UnexpectedToken(Token),
    /// Unexpected end-of-file was encountered.
    #[error("Unexpected end of input")]
    UnexpectedEOFDetected(ModuleId),
    /// Insufficient tokens available to complete parsing.
    #[error("Insufficient tokens `{}`", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
    InsufficientTokens(Token),
    /// Expected a closing parenthesis `)` but found a different delimiter.
    /// The second field is the opening `(` token, if available.
    #[error("Expected a closing parenthesis `)` but got `{}` delimiter", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
    ExpectedClosingParen(Token, Option<Box<Token>>),
    /// Expected a closing brace `}` but found a different delimiter.
    /// The second field is the opening `{` token, if available.
    #[error("Expected a closing brace `}}` but got `{}` delimiter", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
    ExpectedClosingBrace(Token, Option<Box<Token>>),
    /// Expected a closing bracket `]` but found a different delimiter.
    /// The second field is the opening `[` token, if available.
    #[error("Expected a closing bracket `]` but got `{}` delimiter", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
    ExpectedClosingBracket(Token, Option<Box<Token>>),
    /// An invalid assignment target was encountered (expected an identifier).
    #[error("Invalid assignment target: expected an identifier but got `{}`", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
    InvalidAssignmentTarget(Token),
    /// An unknown selector was encountered.
    #[error(transparent)]
    UnknownSelector(selector::UnknownSelector),
    /// A parameter without a default value was found after a parameter with a default value.
    #[error(
        "Non-default parameter `{}` cannot follow a parameter with a default value",
        if .0.is_eof() { "EOF".to_string() } else { .0.to_string() }
    )]
    ParameterWithoutDefaultAfterDefault(Token),
    /// Macro parameters cannot have default values.
    #[error("Macro parameters cannot have default values")]
    MacroParametersCannotHaveDefaults(Token),
    /// A variadic parameter must be the last parameter.
    #[error("Variadic parameter must be the last parameter")]
    VariadicParameterMustBeLast(Token),
    /// Multiple variadic parameters are not allowed.
    #[error("Multiple variadic parameters are not allowed")]
    MultipleVariadicParameters(Token),
    /// Macro parameters cannot be variadic.
    #[error("Macro parameters cannot be variadic")]
    MacroParametersCannotBeVariadic(Token),
    /// The Token is the keyword or operator that required an expression to follow.
    #[error("Expected an expression after `{}` but reached end of file", if .0.is_eof() { "EOF".to_string() } else { .0.to_string() })]
    UnexpectedEOFAfterToken(Token),
    /// An `end` keyword was encountered without a matching block opener.
    #[error("Unexpected `end` keyword — no open block to close")]
    UnmatchedEnd(Token),
}

impl SyntaxError {
    /// Returns the token associated with this error, if available.
    #[cold]
    pub fn token(&self) -> Option<&Token> {
        match self {
            SyntaxError::EnvNotFound(token, _) => Some(token),
            SyntaxError::UnexpectedToken(token) => Some(token),
            SyntaxError::UnexpectedEOFDetected(_) => None,
            SyntaxError::InsufficientTokens(token) => Some(token),
            SyntaxError::ExpectedClosingParen(token, _) => Some(token),
            SyntaxError::ExpectedClosingBrace(token, _) => Some(token),
            SyntaxError::ExpectedClosingBracket(token, _) => Some(token),
            SyntaxError::InvalidAssignmentTarget(token) => Some(token),
            SyntaxError::UnknownSelector(selector::UnknownSelector(token)) => Some(token),
            SyntaxError::ParameterWithoutDefaultAfterDefault(token) => Some(token),
            SyntaxError::MacroParametersCannotHaveDefaults(token) => Some(token),
            SyntaxError::VariadicParameterMustBeLast(token) => Some(token),
            SyntaxError::MultipleVariadicParameters(token) => Some(token),
            SyntaxError::MacroParametersCannotBeVariadic(token) => Some(token),
            SyntaxError::UnexpectedEOFAfterToken(token) => Some(token),
            SyntaxError::UnmatchedEnd(token) => Some(token),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Range, TokenKind, arena::ArenaId, selector};
    use rstest::rstest;

    fn eof_token() -> Token {
        Token {
            range: Range::default(),
            kind: TokenKind::Eof,
            module_id: ArenaId::new(0),
        }
    }

    #[rstest]
    #[case(SyntaxError::EnvNotFound(eof_token(), "VAR".into()), true)]
    #[case(SyntaxError::UnexpectedToken(eof_token()), true)]
    #[case(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)), false)]
    #[case(SyntaxError::InsufficientTokens(eof_token()), true)]
    #[case(SyntaxError::ExpectedClosingParen(eof_token(), None), true)]
    #[case(SyntaxError::ExpectedClosingBrace(eof_token(), None), true)]
    #[case(SyntaxError::ExpectedClosingBracket(eof_token(), None), true)]
    #[case(SyntaxError::InvalidAssignmentTarget(eof_token()), true)]
    #[case(SyntaxError::UnknownSelector(selector::UnknownSelector(eof_token())), true)]
    #[case(SyntaxError::ParameterWithoutDefaultAfterDefault(eof_token()), true)]
    #[case(SyntaxError::MacroParametersCannotHaveDefaults(eof_token()), true)]
    #[case(SyntaxError::VariadicParameterMustBeLast(eof_token()), true)]
    #[case(SyntaxError::MultipleVariadicParameters(eof_token()), true)]
    #[case(SyntaxError::MacroParametersCannotBeVariadic(eof_token()), true)]
    #[case(SyntaxError::UnexpectedEOFAfterToken(eof_token()), true)]
    #[case(SyntaxError::UnmatchedEnd(eof_token()), true)]
    fn test_token_presence(#[case] err: SyntaxError, #[case] has_token: bool) {
        assert_eq!(err.token().is_some(), has_token);
    }

    #[rstest]
    #[case(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)), "Unexpected end of input")]
    #[case(
        SyntaxError::MacroParametersCannotHaveDefaults(eof_token()),
        "Macro parameters cannot have default values"
    )]
    #[case(
        SyntaxError::VariadicParameterMustBeLast(eof_token()),
        "Variadic parameter must be the last parameter"
    )]
    #[case(
        SyntaxError::MultipleVariadicParameters(eof_token()),
        "Multiple variadic parameters are not allowed"
    )]
    #[case(
        SyntaxError::MacroParametersCannotBeVariadic(eof_token()),
        "Macro parameters cannot be variadic"
    )]
    #[case(
        SyntaxError::UnmatchedEnd(eof_token()),
        "Unexpected `end` keyword — no open block to close"
    )]
    fn test_error_display(#[case] err: SyntaxError, #[case] expected: &str) {
        assert_eq!(err.to_string(), expected);
    }
}