use ariadne::{Label, Report, ReportKind, Source};
use core::convert::From;
use std::error::Error;
use std::fmt::{Debug, Display};
pub mod class;
pub mod form;
pub mod lexer;
pub mod module;
pub mod project;
pub mod resource;
pub mod source;
pub use class::ClassError;
pub use form::FormError;
pub use lexer::LexerError;
pub use module::ModuleError;
pub use project::ProjectError;
pub use resource::ResourceError;
pub use source::SourceFileError;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
#[error(transparent)]
Lexer(#[from] LexerError),
#[error(transparent)]
Class(#[from] ClassError),
#[error(transparent)]
Module(#[from] ModuleError),
#[error(transparent)]
Form(#[from] FormError),
#[error(transparent)]
Project(#[from] ProjectError),
#[error(transparent)]
Resource(#[from] ResourceError),
#[error(transparent)]
SourceFile(#[from] SourceFileError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Severity {
Note,
Warning,
#[default]
Error,
}
impl Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Note => write!(f, "note"),
Severity::Warning => write!(f, "warning"),
Severity::Error => write!(f, "error"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Span {
pub offset: u32,
pub line_start: u32,
pub line_end: u32,
pub length: u32,
}
impl Span {
#[must_use]
pub fn new(offset: u32, line_start: u32, line_end: u32, length: u32) -> Self {
Self {
offset,
line_start,
line_end,
length,
}
}
#[must_use]
pub fn zero() -> Self {
Self {
offset: 0,
line_start: 0,
line_end: 0,
length: 0,
}
}
#[must_use]
pub fn at(offset: u32, line: u32) -> Self {
Self {
offset,
line_start: line,
line_end: line,
length: 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticLabel {
pub span: Span,
pub message: String,
}
impl DiagnosticLabel {
pub fn new(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorDetails<'a> {
pub source_name: Box<str>,
pub source_content: &'a str,
pub error_offset: u32,
pub line_start: u32,
pub line_end: u32,
pub kind: Box<ErrorKind>,
pub severity: Severity,
pub labels: Vec<DiagnosticLabel>,
pub notes: Vec<String>,
}
impl<'a> ErrorDetails<'a> {
#[must_use]
pub fn basic<E>(
source_name: Box<str>,
source_content: &'a str,
error_offset: u32,
line_start: u32,
line_end: u32,
kind: E,
severity: Severity,
) -> ErrorDetails<'a>
where
E: Into<ErrorKind>,
{
ErrorDetails {
source_name,
source_content,
error_offset,
line_start,
line_end,
kind: Box::new(kind.into()),
severity,
labels: Vec::new(),
notes: Vec::new(),
}
}
#[must_use]
pub fn with_label(mut self, label: DiagnosticLabel) -> Self {
self.labels.push(label);
self
}
#[must_use]
pub fn with_labels(mut self, labels: Vec<DiagnosticLabel>) -> Self {
self.labels.extend(labels);
self
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
#[must_use]
pub fn with_notes(mut self, notes: Vec<String>) -> Self {
self.notes.extend(notes);
self
}
pub fn emit(self, ctx: &mut ParserContext<'a>) {
ctx.push_error(self);
}
}
impl Display for ErrorDetails<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ErrorDetails {{ source_name: {}, error_offset: {}, line_start: {}, line_end: {}, kind: {:?} }}",
self.source_name,
self.error_offset,
self.line_start,
self.line_end,
self.kind,
)
}
}
impl ErrorDetails<'_> {
pub fn print(&self) {
let cache = (
self.source_name.to_string(),
Source::from(self.source_content),
);
let mut report = Report::build(
ReportKind::Error,
(
self.source_name.to_string(),
self.line_start as usize..=self.line_end as usize,
),
)
.with_message(self.kind.to_string());
if self.labels.is_empty() {
let span_start = self.error_offset as usize;
let span_end = self.error_offset as usize;
report = report.with_label(
Label::new((self.source_name.to_string(), span_start..=span_end))
.with_message("error here"),
);
} else {
for label in &self.labels {
let span_start = label.span.offset as usize;
let span_end = (label.span.offset + label.span.length.max(1) - 1) as usize;
report = report.with_label(
Label::new((self.source_name.to_string(), span_start..=span_end))
.with_message(&label.message),
);
}
}
for note in &self.notes {
report = report.with_note(note);
}
let result = report.finish().print(cache);
if let Some(e) = result.err() {
eprint!("Error attempting to build ErrorDetails print message {e:?}");
}
}
pub fn eprint(&self) {
let cache = (
self.source_name.to_string(),
Source::from(self.source_content),
);
let mut report = Report::build(
ReportKind::Error,
(
self.source_name.to_string(),
self.line_start as usize..=self.line_end as usize,
),
)
.with_message(format!("{:?}", self.kind));
if self.labels.is_empty() {
report = report.with_label(
Label::new((
self.source_name.to_string(),
self.error_offset as usize..=self.error_offset as usize,
))
.with_message("error here"),
);
} else {
for label in &self.labels {
report = report.with_label(
Label::new((
self.source_name.to_string(),
label.span.offset as usize
..=(label.span.offset + label.span.length.max(1) - 1) as usize,
))
.with_message(&label.message),
);
}
}
for note in &self.notes {
report = report.with_note(note);
}
let result = report.finish().eprint(cache);
if let Some(e) = result.err() {
eprint!("Error attempting to build ErrorDetails eprint message {e:?}");
}
}
pub fn print_to_string(&self) -> Result<String, Box<dyn Error>> {
let cache = (
self.source_name.to_string(),
Source::from(self.source_content),
);
let mut buf = Vec::new();
let mut report = Report::build(
ReportKind::Error,
(
self.source_name.to_string(),
self.line_start as usize..=self.line_end as usize,
),
)
.with_message(self.kind.to_string());
if self.labels.is_empty() {
report = report.with_label(
Label::new((
self.source_name.to_string(),
self.error_offset as usize..=self.error_offset as usize,
))
.with_message("error here"),
);
} else {
for label in &self.labels {
report = report.with_label(
Label::new((
self.source_name.to_string(),
label.span.offset as usize
..=(label.span.offset + label.span.length.max(1) - 1) as usize,
))
.with_message(&label.message),
);
}
}
let _ = report.finish().write(cache, &mut buf);
let text = String::from_utf8(buf.clone())?;
Ok(text)
}
}
#[derive(Debug)]
pub struct ParserContext<'a> {
file_name: String,
contents: &'a str,
errors: Vec<ErrorDetails<'a>>,
}
impl<'a> ParserContext<'a> {
pub fn new<S: Into<String>>(file_name: S, contents: &'a str) -> Self {
Self {
file_name: file_name.into(),
contents,
errors: Vec::new(),
}
}
pub fn error<E>(&mut self, span: Span, kind: E)
where
E: Into<ErrorKind>,
{
let error = ErrorDetails {
source_name: self.file_name.clone().into_boxed_str(),
source_content: self.contents,
error_offset: span.offset,
line_start: span.line_start,
line_end: span.line_end,
kind: Box::new(kind.into()),
severity: Severity::Error,
labels: vec![],
notes: vec![],
};
self.errors.push(error);
}
pub fn error_with<E>(&self, span: Span, kind: E) -> ErrorDetails<'a>
where
E: Into<ErrorKind>,
{
ErrorDetails {
source_name: self.file_name.clone().into_boxed_str(),
source_content: self.contents,
error_offset: span.offset,
line_start: span.line_start,
line_end: span.line_end,
kind: Box::new(kind.into()),
severity: Severity::Error,
labels: vec![],
notes: vec![],
}
}
pub fn push_error(&mut self, error: ErrorDetails<'a>) {
self.errors.push(error);
}
pub fn extend_errors(&mut self, errors: impl IntoIterator<Item = ErrorDetails<'a>>) {
self.errors.extend(errors);
}
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
#[must_use]
pub fn error_count(&self) -> usize {
self.errors.len()
}
#[must_use]
pub fn errors(&self) -> &[ErrorDetails<'a>] {
&self.errors
}
pub fn take_errors(&mut self) -> Vec<ErrorDetails<'a>> {
std::mem::take(&mut self.errors)
}
#[must_use]
pub fn into_errors(self) -> Vec<ErrorDetails<'a>> {
self.errors
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[must_use]
pub fn contents(&self) -> &'a str {
self.contents
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::SourceStream;
use assert_matches::assert_matches;
#[test]
fn test_automatic_error_conversion() {
let stream = SourceStream::new("test.bas", "Dim x As Integer");
let mut ctx = ParserContext::new(stream.file_name(), stream.contents);
ctx.error(
stream.span_here(),
LexerError::UnknownToken {
token: "???".to_string(),
},
);
ctx.error(stream.span_here(), ModuleError::AttributeKeywordMissing);
ctx.error(stream.span_here(), ClassError::VersionKeywordMissing);
ctx.error(stream.span_here(), ProjectError::UnterminatedSectionHeader);
let _error5 = ErrorDetails::basic(
"test.bas".to_string().into_boxed_str(),
"Dim x",
0,
0,
5,
FormError::VersionKeywordMissing,
Severity::Error,
);
assert_eq!(ctx.error_count(), 4);
assert!(ctx.has_errors());
}
#[test]
fn test_error_kind_conversion() {
let lexer_err: ErrorKind = LexerError::UnknownToken {
token: "test".to_string(),
}
.into();
assert_matches!(lexer_err, ErrorKind::Lexer(_));
let module_err: ErrorKind = ModuleError::AttributeKeywordMissing.into();
assert_matches!(module_err, ErrorKind::Module(_));
let class_err: ErrorKind = ClassError::VersionKeywordMissing.into();
assert_matches!(class_err, ErrorKind::Class(_));
let project_err: ErrorKind = ProjectError::UnterminatedSectionHeader.into();
assert_matches!(project_err, ErrorKind::Project(_));
let form_err: ErrorKind = FormError::VersionKeywordMissing.into();
assert_matches!(form_err, ErrorKind::Form(_));
let resource_err: ErrorKind = ResourceError::OffsetOutOfBounds {
offset: 0,
file_length: 10,
}
.into();
assert_matches!(resource_err, ErrorKind::Resource(_));
let source_err: ErrorKind = SourceFileError::Malformed {
message: "test".to_string(),
}
.into();
assert_matches!(source_err, ErrorKind::SourceFile(_));
}
}