rd-source 0.0.1

Parser for R documentation source files into rd-ast documents
Documentation
use std::{fmt, ops::Range};

use rd_ast::RdDocument;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourcePosition {
    line: u32,
    column: u32,
}
impl SourcePosition {
    pub fn line(&self) -> u32 {
        self.line
    }
    pub fn column(&self) -> u32 {
        self.column
    }
}
impl SourcePosition {
    pub(crate) fn new(line: u32, column: u32) -> Self {
        Self { line, column }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceSpan {
    bytes: Range<usize>,
    start: SourcePosition,
    end: SourcePosition,
}
impl SourceSpan {
    pub(crate) fn new(bytes: Range<usize>, start: SourcePosition, end: SourcePosition) -> Self {
        Self { bytes, start, end }
    }
}
impl SourceSpan {
    pub fn bytes(&self) -> Range<usize> {
        self.bytes.clone()
    }
    pub fn start(&self) -> &SourcePosition {
        &self.start
    }
    pub fn end(&self) -> &SourcePosition {
        &self.end
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct Parsed {
    document: RdDocument,
    diagnostics: Vec<Diagnostic>,
}
impl Parsed {
    pub(crate) fn new(document: RdDocument, diagnostics: Vec<Diagnostic>) -> Self {
        Self {
            document,
            diagnostics,
        }
    }
    pub fn document(&self) -> &RdDocument {
        &self.document
    }
    pub fn diagnostics(&self) -> &[Diagnostic] {
        &self.diagnostics
    }
    pub fn into_parts(self) -> (RdDocument, Vec<Diagnostic>) {
        (self.document, self.diagnostics)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
    severity: Severity,
    code: DiagnosticCode,
    message: String,
    span: SourceSpan,
}
impl Diagnostic {
    pub(crate) fn new(
        severity: Severity,
        code: DiagnosticCode,
        message: impl Into<String>,
        span: SourceSpan,
    ) -> Self {
        Self {
            severity,
            code,
            message: message.into(),
            span,
        }
    }
    pub fn severity(&self) -> &Severity {
        &self.severity
    }
    pub fn code(&self) -> &DiagnosticCode {
        &self.code
    }
    pub fn message(&self) -> &str {
        &self.message
    }
    pub fn span(&self) -> &SourceSpan {
        &self.span
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Severity {
    Warning,
    Error,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DiagnosticCode {
    UnexpectedClosingDelimiter,
    UnexpectedOpeningDelimiter,
    UnclosedGroup,
    UnclosedOption,
    MissingArgument,
    UnknownTag,
    TagNotAllowedHere,
    InvalidOptionSyntax,
    InvalidArity,
    UnexpectedToken,
    UnexpectedEndIf,
    MissingEndIf,
    UnexpectedConditional,
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
    InvalidUtf8 {
        valid_up_to: usize,
        error_len: Option<usize>,
    },
    NulByte {
        offset: usize,
    },
    UnsupportedEncoding {
        name: String,
        span: Option<SourceSpan>,
    },
    InputTooLarge,
    NestingLimitExceeded {
        span: SourceSpan,
    },
}
impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidUtf8 {
                valid_up_to,
                error_len,
            } => write!(f, "invalid UTF-8 at {valid_up_to} (length {error_len:?})"),
            Self::NulByte { offset } => write!(f, "embedded NUL byte at {offset}"),
            Self::UnsupportedEncoding { name, .. } => write!(f, "unsupported encoding: {name}"),
            Self::InputTooLarge => f.write_str("input is too large"),
            Self::NestingLimitExceeded { .. } => f.write_str("nesting limit exceeded"),
        }
    }
}
impl std::error::Error for ParseError {}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn hard_input_errors_report_utf8_and_nul() {
        assert_eq!(
            crate::parse(b"a\0b"),
            Err(ParseError::NulByte { offset: 1 })
        );
        assert_eq!(
            crate::parse(&[b'a', 0xe2, 0x82]),
            Err(ParseError::InvalidUtf8 {
                valid_up_to: 1,
                error_len: None
            })
        );
        assert_eq!(
            crate::parse(&[0xff]),
            Err(ParseError::InvalidUtf8 {
                valid_up_to: 0,
                error_len: Some(1)
            })
        );
    }
    #[test]
    fn empty_and_whitespace_documents_are_preserved() {
        assert!(crate::parse(b"").unwrap().document().nodes().is_empty());
        let document = crate::parse(b" \t\n").unwrap().into_parts().0;
        assert_eq!(document.nodes(), &[rd_ast::RdNode::Text(" \t\n".into())]);
    }
}