use std::borrow::Cow;
use std::cell::Cell;
use std::cmp::Ordering;
use std::io::IsTerminal;
use std::io::Write;
use std::ops::Range;
use codespan_reporting::diagnostic::Diagnostic;
use codespan_reporting::diagnostic::Label;
use codespan_reporting::diagnostic::LabelStyle;
use codespan_reporting::diagnostic::Severity;
use codespan_reporting::files::Error;
use codespan_reporting::files::Files;
use codespan_reporting::term;
use codespan_reporting::term::Config;
use codespan_reporting::term::DisplayStyle;
use mago_database::file::FileId;
use termcolor::Buffer;
use mago_database::DatabaseReader;
use mago_database::ReadDatabase;
use crate::Annotation;
use crate::AnnotationKind;
use crate::Issue;
use crate::IssueCollection;
use crate::Level;
use crate::error::ReportingError;
use crate::formatter::Formatter;
use crate::formatter::FormatterConfig;
use crate::formatter::utils::osc8_hyperlink;
pub(crate) struct RichFormatter;
impl Formatter for RichFormatter {
fn format(
&self,
writer: &mut dyn Write,
issues: &IssueCollection,
database: &ReadDatabase,
config: &FormatterConfig,
) -> Result<(), ReportingError> {
codespan_format_with_config(
writer,
issues,
database,
config,
&Config { display_style: DisplayStyle::Rich, ..Default::default() },
)
}
}
pub(super) fn codespan_format_with_config(
writer: &mut dyn Write,
issues: &IssueCollection,
database: &ReadDatabase,
config: &FormatterConfig,
codespan_config: &Config,
) -> Result<(), ReportingError> {
let use_colors = config.color_choice.should_use_colors(std::io::stdout().is_terminal());
let mut buffer = if use_colors { Buffer::ansi() } else { Buffer::no_color() };
let editor_url = if use_colors { config.editor_url.as_deref() } else { None };
let files = DatabaseFiles { database, editor_url, line_hint: Cell::new(None), column_hint: Cell::new(None) };
let mut highest_level: Option<Level> = None;
let mut errors = 0;
let mut warnings = 0;
let mut notes = 0;
let mut help = 0;
let mut suggestions = 0;
for issue in crate::formatter::utils::filter_issues(issues, config, true) {
match issue.level {
Level::Note => notes += 1,
Level::Help => help += 1,
Level::Warning => warnings += 1,
Level::Error => errors += 1,
}
highest_level = Some(highest_level.map_or(issue.level, |cur| cur.max(issue.level)));
if !issue.edits.is_empty() {
suggestions += 1;
}
if editor_url.is_some() {
if let Some(annotation) = issue.annotations.iter().find(|a| a.is_primary()) {
if let Ok(file) = database.get_ref(&annotation.span.file_id) {
let line = file.line_number(annotation.span.start.offset) + 1;
let column = file.column_number(annotation.span.start.offset) + 1;
files.line_hint.set(Some(line));
files.column_hint.set(Some(column));
}
} else {
files.line_hint.set(None);
files.column_hint.set(None);
}
}
let diagnostic: Diagnostic<FileId> = issue.into();
term::emit_to_write_style(&mut buffer, codespan_config, &files, &diagnostic)?;
}
if let Some(highest_level) = highest_level {
let total_issues = errors + warnings + notes + help;
let mut message_notes = vec![];
if errors > 0 {
message_notes.push(format!("{errors} error(s)"));
}
if warnings > 0 {
message_notes.push(format!("{warnings} warning(s)"));
}
if notes > 0 {
message_notes.push(format!("{notes} note(s)"));
}
if help > 0 {
message_notes.push(format!("{help} help message(s)"));
}
let mut diagnostic: Diagnostic<FileId> = Diagnostic::new(highest_level.into()).with_message(format!(
"found {} issues: {}",
total_issues,
message_notes.join(", ")
));
if suggestions > 0 {
diagnostic = diagnostic.with_notes(vec![format!("{} issues contain auto-fix suggestions", suggestions)]);
}
term::emit_to_write_style(&mut buffer, codespan_config, &files, &diagnostic)?;
}
writer.write_all(buffer.as_slice())?;
Ok(())
}
struct DatabaseFiles<'a> {
database: &'a ReadDatabase,
editor_url: Option<&'a str>,
line_hint: Cell<Option<u32>>,
column_hint: Cell<Option<u32>>,
}
impl<'a> Files<'a> for DatabaseFiles<'_> {
type FileId = FileId;
type Name = Cow<'a, str>;
type Source = &'a str;
fn name(&'a self, file_id: FileId) -> Result<Cow<'a, str>, Error> {
let file = self.database.get_ref(&file_id).map_err(|_| Error::FileMissing)?;
let name = file.name.as_ref();
if let (Some(template), Some(path)) = (self.editor_url, file.path.as_ref()) {
let abs_path = path.display().to_string();
let line = self.line_hint.get().unwrap_or(1);
let column = self.column_hint.get().unwrap_or(1);
Ok(Cow::Owned(osc8_hyperlink(template, &abs_path, line, column, name)))
} else {
Ok(Cow::Borrowed(name))
}
}
fn source(&'a self, file_id: FileId) -> Result<&'a str, Error> {
self.database.get_ref(&file_id).map(|source| source.contents.as_ref()).map_err(|_| Error::FileMissing)
}
fn line_index(&self, file_id: FileId, byte_index: usize) -> Result<usize, Error> {
let file = self.database.get_ref(&file_id).map_err(|_| Error::FileMissing)?;
Ok(file.line_number(
byte_index.try_into().map_err(|_| Error::IndexTooLarge { given: byte_index, max: u32::MAX as usize })?,
) as usize)
}
fn line_range(&self, file_id: FileId, line_index: usize) -> Result<Range<usize>, Error> {
let file = self.database.get(&file_id).map_err(|_| Error::FileMissing)?;
codespan_line_range(&file.lines, file.size, line_index)
}
}
fn codespan_line_start(lines: &[u32], size: u32, line_index: usize) -> Result<usize, Error> {
match line_index.cmp(&lines.len()) {
Ordering::Less => Ok(lines.get(line_index).copied().expect("failed despite previous check") as usize),
Ordering::Equal => Ok(size as usize),
Ordering::Greater => Err(Error::LineTooLarge { given: line_index, max: lines.len() - 1 }),
}
}
fn codespan_line_range(lines: &[u32], size: u32, line_index: usize) -> Result<Range<usize>, Error> {
let line_start = codespan_line_start(lines, size, line_index)?;
let next_line_start = codespan_line_start(lines, size, line_index + 1)?;
Ok(line_start..next_line_start)
}
impl From<AnnotationKind> for LabelStyle {
fn from(kind: AnnotationKind) -> LabelStyle {
match kind {
AnnotationKind::Primary => LabelStyle::Primary,
AnnotationKind::Secondary => LabelStyle::Secondary,
}
}
}
impl From<Annotation> for Label<FileId> {
fn from(annotation: Annotation) -> Label<FileId> {
let mut label = Label::new(annotation.kind.into(), annotation.span.file_id, annotation.span);
if let Some(message) = annotation.message {
label.message = message;
}
label
}
}
impl From<&Annotation> for Label<FileId> {
fn from(annotation: &Annotation) -> Label<FileId> {
let mut label = Label::new(annotation.kind.into(), annotation.span.file_id, annotation.span);
if let Some(message) = &annotation.message {
label.message = message.clone();
}
label
}
}
impl From<Level> for Severity {
fn from(level: Level) -> Severity {
match level {
Level::Note => Severity::Note,
Level::Help => Severity::Help,
Level::Warning => Severity::Warning,
Level::Error => Severity::Error,
}
}
}
impl From<Issue> for Diagnostic<FileId> {
fn from(issue: Issue) -> Diagnostic<FileId> {
let mut diagnostic = Diagnostic::new(issue.level.into()).with_message(issue.message);
if let Some(code) = issue.code {
diagnostic.code = Some(code);
}
for annotation in issue.annotations {
diagnostic.labels.push(annotation.into());
}
for note in issue.notes {
diagnostic.notes.push(note);
}
if let Some(help) = issue.help {
diagnostic.notes.push(format!("Help: {help}"));
}
if let Some(link) = issue.link {
diagnostic.notes.push(format!("See: {link}"));
}
diagnostic
}
}
impl From<&Issue> for Diagnostic<FileId> {
fn from(issue: &Issue) -> Diagnostic<FileId> {
let mut diagnostic = Diagnostic::new(issue.level.into()).with_message(issue.message.clone());
if let Some(code) = &issue.code {
diagnostic.code = Some(code.clone());
}
for annotation in &issue.annotations {
diagnostic.labels.push(annotation.into());
}
for note in &issue.notes {
diagnostic.notes.push(note.clone());
}
if let Some(help) = &issue.help {
diagnostic.notes.push(format!("Help: {help}"));
}
if let Some(link) = &issue.link {
diagnostic.notes.push(format!("See: {link}"));
}
diagnostic
}
}