use std::fmt;
use ariadne::{Config, Label, Report, ReportKind, Source};
use vb6core::error::{err_number, VBError};
#[derive(Debug, Clone)]
pub struct BuiltinCallInfo {
pub param_index: usize,
pub param_name: String,
pub arg_byte_ranges: Option<Vec<(u32, u32)>>,
}
#[derive(Debug, Clone)]
pub struct RunError {
pub error: Box<VBError>,
pub is_debug_pause: bool,
pub line: Option<usize>,
pub procedure: Option<String>,
pub builtin_call: Option<BuiltinCallInfo>,
}
impl RunError {
pub fn new(error: VBError) -> Self {
Self {
error: Box::new(error),
is_debug_pause: false,
line: None,
procedure: None,
builtin_call: None,
}
}
pub fn debug_pause() -> Self {
Self {
error: Box::new(VBError::new(0)),
is_debug_pause: true,
line: None,
procedure: None,
builtin_call: None,
}
}
pub fn at_line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
pub fn in_procedure(mut self, name: &str) -> Self {
self.procedure = Some(name.to_string());
self
}
pub fn with_builtin_call(mut self, info: Option<BuiltinCallInfo>) -> Self {
self.builtin_call = info;
self
}
pub fn err_number(number: i32) -> Self {
Self::new(VBError::new(number))
}
pub fn invalid_procedure_call() -> Self {
Self::new(VBError::invalid_procedure_call())
}
pub fn type_mismatch() -> Self {
Self::new(VBError::type_mismatch())
}
pub fn sub_or_function_not_defined() -> Self {
Self::new(VBError::new(err_number::SUB_OR_FUNCTION_NOT_DEFINED))
}
pub fn is_debug_pause(&self) -> bool {
self.is_debug_pause
}
}
impl fmt::Display for RunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_debug_pause {
if let Some(line) = self.line {
return write!(f, "paused at line {line}");
}
return f.write_str("paused before next statement");
}
if let Some(line) = self.line {
write!(f, "line {line}: {}", self.error)
} else {
write!(f, "{}", self.error)
}
}
}
impl std::error::Error for RunError {}
impl From<VBError> for RunError {
fn from(error: VBError) -> Self {
Self::new(error)
}
}
impl From<RunError> for VBError {
fn from(run_error: RunError) -> Self {
*run_error.error
}
}
pub fn render_error_report(
source_name: &str,
source: &str,
error: &RunError,
line_offset: usize,
) -> Option<String> {
if error.is_debug_pause {
return None;
}
let line = error.line? + line_offset;
let (span_start, span_end) = if let Some(ref call) = error.builtin_call {
if let Some(ref ranges) = call.arg_byte_ranges {
if let Some(&(start, end)) = ranges.get(call.param_index) {
(start as usize, end as usize)
} else {
line_byte_span(source, line)?
}
} else {
line_byte_span(source, line)?
}
} else {
line_byte_span(source, line)?
};
let label_message = if let Some(ref call) = error.builtin_call {
format!("'{}' parameter", call.param_name)
} else {
"error here".to_string()
};
render_report_with_label(
source_name,
source,
line,
&error.error.to_string(),
span_start,
span_end,
&label_message,
)
}
fn render_report_with_label(
source_name: &str,
source: &str,
_line: usize,
message: &str,
span_start: usize,
span_end: usize,
label: &str,
) -> Option<String> {
let cache = (source_name.to_string(), Source::from(source));
let mut buf = Vec::new();
let report = Report::build(
ReportKind::Error,
(source_name.to_string(), span_start..=span_end),
)
.with_message(message)
.with_label(Label::new((source_name.to_string(), span_start..=span_end)).with_message(label))
.with_config(Config::new().with_color(false));
report.finish().write(cache, &mut buf).ok()?;
String::from_utf8(buf).ok()
}
pub fn render_report_at_line(
source_name: &str,
source: &str,
line: usize,
message: &str,
) -> Option<String> {
let (span_start, span_end) = line_byte_span(source, line)?;
let cache = (source_name.to_string(), Source::from(source));
let mut buf = Vec::new();
let report = Report::build(
ReportKind::Error,
(source_name.to_string(), span_start..=span_end),
)
.with_message(message)
.with_label(
Label::new((source_name.to_string(), span_start..=span_end)).with_message("error here"),
)
.with_config(Config::new().with_color(false));
report.finish().write(cache, &mut buf).ok()?;
String::from_utf8(buf).ok()
}
fn line_byte_span(source: &str, line: usize) -> Option<(usize, usize)> {
let mut start = 0usize;
for (index, part) in source.split_inclusive('\n').enumerate() {
let line_no = index + 1;
if line_no == line {
let trimmed = part.trim_end_matches(['\r', '\n']);
let end = start + trimmed.len();
return Some((start, end.max(start + 1)));
}
start += part.len();
}
None
}
pub type RunResult<T> = Result<T, RunError>;
#[cfg(test)]
mod tests {
use vb6core::error::err_number;
use super::*;
#[test]
fn report_points_at_the_offending_line() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Dim x As Double\n\
x = 1 / 0\n\
End Sub\n";
let error = RunError::new(VBError::new(err_number::DIVISION_BY_ZERO))
.at_line(3)
.in_procedure("Main");
let report = render_error_report("scratch.bas", source, &error, 1).unwrap();
assert!(report.contains("Runtime error 11") || report.contains("Error 11"));
assert!(report.contains("scratch.bas:4"));
assert!(report.contains("x = 1 / 0"));
}
#[test]
fn report_includes_error_number_and_description() {
let source = "Sub Main()\n Debug.Print Missing()\nEnd Sub\n";
let error = RunError::new(VBError::new(err_number::WRONG_NUMBER_OF_ARGUMENTS)).at_line(2);
let report = render_error_report("m.bas", source, &error, 0).unwrap();
assert!(report.contains("450"));
assert!(report.contains("Wrong number of arguments"));
}
#[test]
fn report_is_none_for_debug_pause_or_missing_line() {
let source = "Sub Main()\nEnd Sub\n";
let pause = RunError::debug_pause();
assert!(render_error_report("m.bas", source, &pause, 0).is_none());
let no_line = RunError::new(VBError::new(err_number::TYPE_MISMATCH));
assert!(render_error_report("m.bas", source, &no_line, 0).is_none());
}
#[test]
fn report_is_none_when_line_is_out_of_range() {
let source = "Sub Main()\nEnd Sub\n";
let error = RunError::new(VBError::new(err_number::TYPE_MISMATCH)).at_line(99);
assert!(render_error_report("m.bas", source, &error, 0).is_none());
}
#[test]
fn report_at_line_uses_provided_message() {
let source = "Dim x As ?\nSub Main()\nEnd Sub\n";
let report = render_report_at_line("m.bas", source, 1, "Unknown token '?'").unwrap();
assert!(report.contains("Unknown token '?'"));
assert!(report.contains("m.bas:1"));
assert!(report.contains("Dim x As ?"));
assert!(
!report.contains("(Error "),
"parse reports should not carry Err numbers"
);
}
}