use std::fmt::{self, Arguments};
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[cfg(feature = "logger")]
use colored::{Color, Colorize};
#[cfg(feature = "logger")]
use std::{fmt::Write, fs::File, io::BufRead, io::BufReader, io::Result as IoResult};
#[cfg(feature = "logger")]
use unicode_width::UnicodeWidthChar;
#[cfg(feature = "macros")]
pub use laps_macros::Spanned;
#[cfg(not(feature = "logger"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
Normal(String),
Fatal(String),
}
#[cfg(feature = "logger")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
Normal,
Fatal,
}
impl Error {
#[cfg(not(feature = "logger"))]
pub fn is_fatal(&self) -> bool {
matches!(self, Self::Fatal(..))
}
#[cfg(feature = "logger")]
pub fn is_fatal(&self) -> bool {
matches!(self, Self::Fatal)
}
}
pub type Result<T> = std::result::Result<T, Error>;
impl<T> From<Error> for Result<T> {
fn from(error: Error) -> Self {
Err(error)
}
}
#[derive(Clone)]
pub struct Span {
status: Arc<LoggerStatus>,
start: Location,
end: Location,
}
#[cfg(feature = "logger")]
macro_rules! get_line_str {
($line:expr) => {
$line
.map_or_else(|| Ok("".into()), |r| r.map_err(|_| fmt::Error))?
.replace('\t', &format!("{:w$}", "", w = Span::TAB_WIDTH))
};
($line:expr, $col:expr) => {{
let line = $line.map_or_else(|| Ok("".into()), |r| r.map_err(|_| fmt::Error))?;
let count = line
.chars()
.take($col as usize)
.map(|c| match c {
'\t' => Span::TAB_WIDTH,
_ => c.width().unwrap_or(0),
})
.sum();
(
line.replace('\t', &format!("{:w$}", "", w = Span::TAB_WIDTH)),
count,
)
}};
($line:expr, $col1:expr, $col2:expr) => {{
let line = $line.map_or_else(|| Ok("".into()), |r| r.map_err(|_| fmt::Error))?;
let col1 = $col1 as usize;
let col2 = $col2 as usize;
let mut iter = line.chars().map(|c| match c {
'\t' => Span::TAB_WIDTH,
_ => c.width().unwrap_or(0),
});
let mut count1 = 0;
for _ in 0..col1 {
count1 += match iter.next() {
Some(n) => n,
None => break,
};
}
let mut count2 = count1;
for _ in col1..col2 {
count2 += match iter.next() {
Some(n) => n,
None => break,
};
}
(
line.replace('\t', &format!("{:w$}", "", w = Span::TAB_WIDTH)),
count1,
count2,
)
}};
}
impl Span {
#[cfg(feature = "logger")]
const TAB_WIDTH: usize = 2;
pub fn new(file_type: FileType) -> Self {
Self {
status: Arc::new(LoggerStatus {
file_type,
errors: AtomicUsize::new(0),
warnings: AtomicUsize::new(0),
}),
start: Location::new(),
end: Location::new(),
}
}
#[cfg(not(feature = "logger"))]
pub fn log_raw_error(&self, args: Arguments) -> Error {
self.status.errors.set(self.status.errors.get() + 1);
Error::Normal(format!("{args}"))
}
#[cfg(feature = "logger")]
pub fn log_raw_error(&self, args: Arguments) -> Error {
self.status.errors.fetch_add(1, Ordering::Relaxed);
eprintln!("{}: {args}", "error".bright_red());
Error::Normal
}
#[cfg(not(feature = "logger"))]
pub fn log_raw_fatal_error(&self, args: Arguments) -> Error {
self.status.errors.set(self.status.errors.get() + 1);
Error::Fatal(format!("{args}"))
}
#[cfg(feature = "logger")]
pub fn log_raw_fatal_error(&self, args: Arguments) -> Error {
self.status.errors.fetch_add(1, Ordering::Relaxed);
eprintln!("{}: {args}", "error".bright_red());
Error::Fatal
}
#[cfg(not(feature = "logger"))]
pub fn log_raw_warning(&self, _: Arguments) {
self.status.warnings.set(self.status.warnings.get() + 1);
}
#[cfg(feature = "logger")]
pub fn log_raw_warning(&self, args: Arguments) {
self.status.warnings.fetch_add(1, Ordering::Relaxed);
eprintln!("{}: {args}", "warning".yellow());
}
#[cfg(not(feature = "logger"))]
pub fn log_summary(&self) {}
#[cfg(feature = "logger")]
pub fn log_summary(&self) {
let mut msg = String::new();
let errors = self.status.errors.load(Ordering::Relaxed);
let warnings = self.status.warnings.load(Ordering::Relaxed);
if errors != 0 {
let _ = write!(msg, "{errors} error");
if errors > 1 {
msg += "s";
}
}
if errors != 0 && warnings != 0 {
msg += " and ";
}
if warnings != 0 {
let _ = write!(msg, "{warnings} warning");
if warnings > 1 {
msg += "s";
}
}
msg += " emitted";
eprintln!("{}", msg.bold());
}
pub fn file_type(&self) -> &FileType {
&self.status.file_type
}
pub fn error_num(&self) -> usize {
self.status.errors.load(Ordering::Relaxed)
}
pub fn warning_num(&self) -> usize {
self.status.warnings.load(Ordering::Relaxed)
}
#[cfg(not(feature = "logger"))]
pub fn log_error(&self, args: Arguments) -> Error {
self.log_raw_error(args);
Error::Normal(self.error_message(args))
}
#[cfg(feature = "logger")]
pub fn log_error(&self, args: Arguments) -> Error {
self.log_raw_error(args);
self.print_file_info(Color::BrightRed);
Error::Normal
}
#[cfg(not(feature = "logger"))]
pub fn log_fatal_error(&self, args: Arguments) -> Error {
self.log_raw_error(args);
Error::Fatal(self.error_message(args))
}
#[cfg(feature = "logger")]
pub fn log_fatal_error(&self, args: Arguments) -> Error {
self.log_raw_error(args);
self.print_file_info(Color::BrightRed);
Error::Fatal
}
#[cfg(not(feature = "logger"))]
pub fn log_warning(&self, args: Arguments) {
self.log_raw_warning(args);
}
#[cfg(feature = "logger")]
pub fn log_warning(&self, args: Arguments) {
self.log_raw_warning(args);
self.print_file_info(Color::Yellow);
}
pub fn start(&self) -> Location {
self.start
}
pub fn end(&self) -> Location {
self.end
}
pub fn update<C>(&mut self, c: &C)
where
C: LocationUpdate,
{
self.start.update(c);
self.end = self.start;
}
pub fn into_updated<C>(self, c: &C) -> Self
where
C: LocationUpdate,
{
let mut location = self.start;
location.update(c);
Self {
status: self.status,
start: location,
end: location,
}
}
pub fn update_loc(&mut self, loc: Location) {
self.start = loc;
self.end = loc;
}
pub fn update_end<S>(&mut self, span: S)
where
S: AsRef<Self>,
{
self.end = span.as_ref().end;
}
pub fn into_end_updated<S>(self, span: S) -> Self
where
S: AsRef<Self>,
{
Self {
status: self.status,
start: self.start,
end: span.as_ref().end,
}
}
pub fn is_in_same_line_as<S>(&self, span: S) -> bool
where
S: AsRef<Self>,
{
self.end.line == span.as_ref().start.line
}
#[cfg(not(feature = "logger"))]
fn error_message(&self, args: Arguments) -> String {
format!("{}:{}: {args}", self.status.file_type, self.start)
}
#[cfg(feature = "logger")]
fn print_file_info(&self, color: Color) {
let file_type = &self.status.file_type;
eprintln!(" {} {file_type}:{}", "at".blue(), self.start);
if self.start.col > 0 && self.end.col > 0 {
if let FileType::File(path) = file_type {
let _ = File::open(path).map_err(|_| fmt::Error).and_then(|file| {
let mut lines = BufReader::new(file).lines();
if self.start.line == self.end.line {
self.print_single_line_info(&mut lines, color)
} else {
self.print_multi_line_info(&mut lines, color)
}
});
}
}
eprintln!();
}
#[cfg(feature = "logger")]
fn print_single_line_info<T>(&self, lines: &mut T, color: Color) -> fmt::Result
where
T: Iterator<Item = IoResult<String>>,
{
let line_num = self.start.line as usize;
let (line, c1, c2) = get_line_str!(lines.nth(line_num - 1), self.start.col, self.end.col);
let width = ((line_num + 1) as f32).log10().ceil() as usize;
let leading = c1 - 1;
let len = c2 - c1 + 1;
eprintln!("{:width$} {}", "", "|".blue());
eprint!("{} ", format!("{:width$}", line_num).blue());
eprintln!("{} {}", "|".blue(), line);
eprint!("{:width$} {} {:leading$}", "", "|".blue(), "");
eprintln!("{}", format!("{:^>len$}", "").color(color));
Ok(())
}
#[cfg(feature = "logger")]
fn print_multi_line_info<T>(&self, lines: &mut T, color: Color) -> fmt::Result
where
T: Iterator<Item = IoResult<String>>,
{
let mut buf = String::new();
let width = ((self.end.line + 1) as f32).log10().ceil() as usize;
let width = std::cmp::max(width, 2);
let line_num = self.start.line as usize;
let mut lines = lines.skip(line_num - 1);
let (line, start) = get_line_str!(lines.next(), self.start.col);
writeln!(buf, "{:width$} {}", "", "|".blue())?;
write!(buf, "{} ", format!("{:width$}", line_num).blue())?;
writeln!(buf, "{} {line}", "|".blue())?;
write!(buf, "{:width$} {} ", "", "|".blue())?;
writeln!(buf, "{}", format!("{:_>start$}^", "").color(color))?;
let mid_lines = (self.end.line - self.start.line) as usize - 1;
if mid_lines <= 4 {
for i in 0..mid_lines {
let line = get_line_str!(lines.next());
write!(buf, "{} ", format!("{:width$}", line_num + i + 1).blue())?;
writeln!(buf, "{} {} {line}", "|".blue(), "|".color(color))?;
}
} else {
for i in 0..2usize {
let line = get_line_str!(lines.next());
write!(buf, "{} ", format!("{:width$}", line_num + i + 1).blue())?;
writeln!(buf, "{} {} {line}", "|".blue(), "|".color(color))?;
}
writeln!(buf, "{:.>width$} {} {}", "", "|".blue(), "|".color(color))?;
let line = get_line_str!(lines.nth(mid_lines - 3));
write!(buf, "{} ", format!("{:width$}", self.end.line - 1).blue())?;
writeln!(buf, "{} {} {line}", "|".blue(), "|".color(color))?;
}
let line_num = self.end.line as usize;
let (line, end) = get_line_str!(lines.next(), self.end.col);
write!(buf, "{} ", format!("{:width$}", line_num).blue())?;
writeln!(buf, "{} {} {line}", "|".blue(), "|".color(color))?;
write!(buf, "{:width$} {} ", "", "|".blue())?;
if end == 0 {
write!(buf, " ")?;
} else {
write!(buf, "{}", "|".color(color))?;
}
writeln!(buf, "{}", format!("{:_>end$}^", "").color(color))?;
eprint!("{buf}");
Ok(())
}
}
impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}-{}", self.start, self.end)
}
}
impl fmt::Debug for Span {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Span({self})")
}
}
impl PartialEq for Span {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.status, &other.status) && self.start == other.start && self.end == other.end
}
}
impl Eq for Span {}
impl AsRef<Self> for Span {
fn as_ref(&self) -> &Self {
self
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Location {
pub line: u32,
pub col: u32,
}
impl Location {
fn new() -> Self {
Self { line: 1, col: 0 }
}
fn update<C>(&mut self, c: &C)
where
C: LocationUpdate,
{
c.update(self)
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.line, self.col)
}
}
impl fmt::Debug for Location {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Location({self})")
}
}
struct LoggerStatus {
file_type: FileType,
errors: AtomicUsize,
warnings: AtomicUsize,
}
#[derive(Clone)]
pub enum FileType {
File(Box<Path>),
Stdin,
Buffer,
}
impl fmt::Display for FileType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FileType::File(path) => write!(f, "{}", path.display()),
FileType::Stdin => f.write_str("<stdin>"),
FileType::Buffer => f.write_str("<buffer>"),
}
}
}
pub trait Spanned {
fn span(&self) -> Span;
}
impl<T> Spanned for Box<T>
where
T: Spanned,
{
fn span(&self) -> Span {
self.as_ref().span()
}
}
impl<T> Spanned for (T,)
where
T: Spanned,
{
fn span(&self) -> Span {
self.0.span()
}
}
macro_rules! impl_spanned_for_tuple {
($first:ident $last:ident $($ts:ident)*) => {
impl<$first, $($ts,)* $last> Spanned for ($first, $($ts,)* $last)
where
$first: Spanned,
$last: Spanned,
{
fn span(&self) -> Span {
#[allow(non_snake_case, unused_variables)]
let ($first, $($ts,)* $last) = self;
$first.span().into_end_updated($last.span())
}
}
};
}
impl_spanned_for_tuple!(A B);
impl_spanned_for_tuple!(A B C);
impl_spanned_for_tuple!(A B C D);
impl_spanned_for_tuple!(A B C D E);
impl_spanned_for_tuple!(A B C D E F);
impl_spanned_for_tuple!(A B C D E F G);
impl_spanned_for_tuple!(A B C D E F G H);
impl_spanned_for_tuple!(A B C D E F G H I);
impl_spanned_for_tuple!(A B C D E F G H I J);
impl_spanned_for_tuple!(A B C D E F G H I J K);
impl_spanned_for_tuple!(A B C D E F G H I J K L);
impl_spanned_for_tuple!(A B C D E F G H I J K L M);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S T);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S T U);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S T U V);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S T U V W);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S T U V W X);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S T U V W X Y);
impl_spanned_for_tuple!(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z);
pub trait TrySpan {
fn try_span(&self) -> Option<Span>;
}
impl<T> TrySpan for T
where
T: Spanned,
{
fn try_span(&self) -> Option<Span> {
Some(self.span())
}
}
impl<T> TrySpan for Option<T>
where
T: TrySpan,
{
fn try_span(&self) -> Option<Span> {
self.as_ref().and_then(|x| x.try_span())
}
}
impl<T> TrySpan for Vec<T>
where
T: TrySpan,
{
fn try_span(&self) -> Option<Span> {
let first = self.iter().find_map(|x| x.try_span())?;
let last = self.iter().rev().find_map(|x| x.try_span()).unwrap();
Some(first.into_end_updated(last))
}
}
pub trait LocationUpdate {
fn update(&self, loc: &mut Location);
}
impl LocationUpdate for char {
fn update(&self, loc: &mut Location) {
if *self == '\n' {
loc.col = 0;
loc.line += 1;
} else {
loc.col += 1;
}
}
}
impl LocationUpdate for u8 {
fn update(&self, loc: &mut Location) {
(*self as char).update(loc)
}
}
#[macro_export]
macro_rules! log_raw_error {
($span:expr, $($arg:tt)+) => {
$span.log_raw_error(format_args!($($arg)+))
};
}
#[macro_export]
macro_rules! log_raw_fatal_error {
($span:expr, $($arg:tt)+) => {
$span.log_raw_fatal_error(format_args!($($arg)+))
};
}
#[macro_export]
macro_rules! log_raw_warning {
($span:expr, $($arg:tt)+) => {
$span.log_raw_warning(format_args!($($arg)+))
};
}
#[macro_export]
macro_rules! log_error {
($span:expr, $($arg:tt)+) => {
$span.log_error(format_args!($($arg)+))
};
}
#[macro_export]
macro_rules! log_fatal_error {
($span:expr, $($arg:tt)+) => {
$span.log_fatal_error(format_args!($($arg)+))
};
}
#[macro_export]
macro_rules! log_warning {
($span:expr, $($arg:tt)+) => {
$span.log_warning(format_args!($($arg)+))
};
}
#[macro_export]
macro_rules! return_error {
($span:expr, $($arg:tt)+) => {
return $span.log_error(format_args!($($arg)+)).into()
};
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn loc_update() {
let mut loc = Location::new();
assert_eq!(format!("{loc}"), "1:0");
loc.update(&' ');
loc.update(&' ');
assert_eq!(format!("{loc}"), "1:2");
loc.update(&'\n');
assert_eq!(format!("{loc}"), "2:0");
loc.update(&'\n');
loc.update(&'\n');
assert_eq!(format!("{loc}"), "4:0");
}
#[test]
fn span_update() {
let mut span = Span::new(FileType::Buffer);
span.update(&' ');
let sp1 = span.clone();
span.update(&' ');
span.update(&' ');
let sp2 = sp1.clone().into_end_updated(span);
assert!(sp1.is_in_same_line_as(&sp2));
log_error!(sp2, "test error");
log_warning!(sp2, "test warning");
log_warning!(sp2, "test warning 2");
sp2.log_summary();
assert_eq!(format!("{}", sp2.start), "1:1");
assert_eq!(format!("{}", sp2.end), "1:3");
let mut sp = Span::new(FileType::Buffer);
sp.start = Location { line: 10, col: 10 };
sp.end = Location { line: 10, col: 15 };
assert!(!sp2.is_in_same_line_as(&sp));
let sp3 = sp2.clone().into_end_updated(sp);
assert!(sp2.is_in_same_line_as(&sp3));
assert_eq!(format!("{}", sp3.start), "1:1");
assert_eq!(format!("{}", sp3.end), "10:15");
}
}