zigsyn 0.1.0

A pure Rust Zig syntax scanner and parser.
Documentation
use std::fmt;
use std::path::PathBuf;

use crate::token::{Token, TokenKind};

#[derive(Debug)]
pub enum Error {
    IO(std::io::Error),
    UnexpectedToken {
        path: PathBuf,
        location: (usize, usize),
        expect: Vec<TokenKind>,
        actual: Option<Token>,
    },
    Else {
        path: PathBuf,
        location: (usize, usize),
        reason: String,
    },
}

impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Self {
        Self::IO(err)
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::IO(err) => Some(err),
            _ => None,
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::IO(err) => write!(f, "{err}"),
            Self::UnexpectedToken {
                path,
                location,
                expect,
                actual,
            } => {
                write!(
                    f,
                    "{}:{}:{} expected ",
                    path.display(),
                    location.0,
                    location.1
                )?;
                write_expectations(f, expect)?;
                match actual {
                    Some(token) => write!(f, ", found {token}"),
                    None => write!(f, ", found EOF"),
                }
            }
            Self::Else {
                path,
                location,
                reason,
            } => write!(
                f,
                "{}:{}:{} {}",
                path.display(),
                location.0,
                location.1,
                reason
            ),
        }
    }
}

fn write_expectations(f: &mut fmt::Formatter<'_>, expect: &[TokenKind]) -> fmt::Result {
    match expect {
        [] => write!(f, "valid token"),
        [single] => write!(f, "{single}"),
        many => {
            for (idx, token) in many.iter().enumerate() {
                if idx > 0 {
                    write!(f, " or ")?;
                }
                write!(f, "{token}")?;
            }
            Ok(())
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use crate::token::{Keyword, Symbol, Token};

    use super::*;

    #[test]
    fn formats_unexpected_single_token() {
        let err = Error::UnexpectedToken {
            path: PathBuf::from("x.zig"),
            location: (2, 3),
            expect: vec![TokenKind::Symbol(Symbol::Semicolon)],
            actual: Some(Token::Keyword(Keyword::Fn)),
        };

        assert_eq!(err.to_string(), "x.zig:2:3 expected ';', found 'fn'");
    }

    #[test]
    fn formats_unexpected_multiple_tokens() {
        let err = Error::UnexpectedToken {
            path: PathBuf::from("x.zig"),
            location: (4, 5),
            expect: vec![
                TokenKind::Symbol(Symbol::Comma),
                TokenKind::Symbol(Symbol::RBrace),
            ],
            actual: Some(Token::Symbol(Symbol::Semicolon)),
        };

        assert_eq!(err.to_string(), "x.zig:4:5 expected ',' or '}', found ';'");
    }

    #[test]
    fn formats_unexpected_eof_and_else() {
        let err = Error::UnexpectedToken {
            path: PathBuf::from("x.zig"),
            location: (10, 1),
            expect: vec![TokenKind::Symbol(Symbol::RParen)],
            actual: None,
        };
        assert_eq!(err.to_string(), "x.zig:10:1 expected ')', found EOF");

        let err = Error::Else {
            path: PathBuf::from("x.zig"),
            location: (1, 1),
            reason: "invalid input".to_owned(),
        };
        assert_eq!(err.to_string(), "x.zig:1:1 invalid input");
    }
}