use super::{Diag, Emitter};
use crate::{
SourceMap, Span,
diagnostics::{
ConfusionType, DiagId, DiagMsg, Level, MultiSpan, SpanLabel, Style, SubDiagnostic,
SuggestionStyle, Suggestions, detect_confusion_type, emitter::normalize_whitespace,
is_different,
},
source_map::{FileName, SourceFile},
};
use annotate_snippets::{
Annotation as ASAnnotation, AnnotationKind, Group, Level as ASLevel, Padding, Patch, Renderer,
Snippet, renderer::DecorStyle,
};
use anstream::{AutoStream, ColorChoice};
use solar_config::HumanEmitterKind;
use solar_data_structures::pluralize;
use std::{
any::Any,
borrow::Cow,
io::{self, Write},
sync::{Arc, OnceLock},
};
type Writer = dyn Write + Send + 'static;
pub(super) const MAX_SUGGESTIONS: usize = 4;
const DEFAULT_RENDERER: Renderer = Renderer::styled()
.error(Level::Error.style())
.warning(Level::Warning.style())
.note(Level::Note.style())
.help(Level::Help.style())
.line_num(Style::LineNumber.to_color_spec(Level::Note))
.addition(Style::Addition.to_color_spec(Level::Note))
.removal(Style::Removal.to_color_spec(Level::Note))
.context(Style::LabelSecondary.to_color_spec(Level::Note));
pub struct HumanEmitter {
writer_type_id: std::any::TypeId,
real_writer: *mut Writer,
writer: AutoStream<Box<Writer>>,
source_map: Option<Arc<SourceMap>>,
renderer: Renderer,
}
unsafe impl Send for HumanEmitter {}
impl Emitter for HumanEmitter {
fn emit_diagnostic(&mut self, diag: &mut Diag) {
let mut primary_span = Cow::Owned(std::mem::take(&mut diag.span));
let mut suggestions = Cow::Owned(std::mem::take(&mut diag.suggestions));
self.primary_span_formatted(&mut primary_span, &mut suggestions);
self.emit_messages_default(
&diag.level,
&diag.messages,
&diag.code,
&primary_span,
&diag.children,
&suggestions,
);
diag.span = primary_span.into_owned();
diag.suggestions = suggestions.into_owned();
}
fn emit_diagnostic_ref(&mut self, diag: &Diag) {
let mut primary_span = Cow::Borrowed(&diag.span);
let mut suggestions = Cow::Borrowed(&diag.suggestions);
self.primary_span_formatted(&mut primary_span, &mut suggestions);
self.emit_messages_default(
&diag.level,
&diag.messages,
&diag.code,
&primary_span,
&diag.children,
&suggestions,
);
}
fn source_map(&self) -> Option<&Arc<SourceMap>> {
self.source_map.as_ref()
}
fn supports_color(&self) -> bool {
match self.writer.current_choice() {
ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
ColorChoice::Auto | ColorChoice::Never => false,
}
}
}
impl HumanEmitter {
pub fn new<W: Write + Send + 'static>(writer: W, color: ColorChoice) -> Self {
let writer_type_id = writer.type_id();
let mut real_writer = Box::new(writer) as Box<Writer>;
Self {
writer_type_id,
real_writer: &mut *real_writer,
writer: AutoStream::new(real_writer, color),
source_map: None,
renderer: DEFAULT_RENDERER,
}
.human_kind(Default::default())
}
pub fn test() -> Self {
struct TestWriter;
impl Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
eprint!("{}", String::from_utf8_lossy(buf));
Ok(buf.len())
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.write(buf).map(drop)
}
fn flush(&mut self) -> io::Result<()> {
io::stderr().flush()
}
}
Self::new(TestWriter, ColorChoice::Always)
}
pub fn stderr(color_choice: ColorChoice) -> Self {
Self::new(io::BufWriter::new(io::stderr()), stderr_choice(color_choice))
}
pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
self.set_source_map(source_map);
self
}
pub fn set_source_map(&mut self, source_map: Option<Arc<SourceMap>>) {
self.source_map = source_map;
}
pub fn ui_testing(mut self, yes: bool) -> Self {
self.renderer = self.renderer.anonymized_line_numbers(yes);
self
}
pub fn set_ui_testing(&mut self, yes: bool) {
self.renderer =
std::mem::replace(&mut self.renderer, DEFAULT_RENDERER).anonymized_line_numbers(yes);
}
pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
match kind {
HumanEmitterKind::Ascii => {
self.renderer = self.renderer.decor_style(DecorStyle::Ascii);
}
HumanEmitterKind::Unicode => {
self.renderer = self.renderer.decor_style(DecorStyle::Unicode);
}
HumanEmitterKind::Short => {
self.renderer = self.renderer.short_message(true);
}
_ => unimplemented!("{kind:?}"),
}
self
}
pub fn terminal_width(mut self, width: Option<usize>) -> Self {
if let Some(w) = width {
self.renderer = self.renderer.term_width(w);
}
self
}
fn downcast_writer<T: Any>(&self) -> Option<&T> {
if self.writer_type_id == std::any::TypeId::of::<T>() {
Some(unsafe { &*(self.real_writer as *const T) })
} else {
None
}
}
fn downcast_writer_mut<T: Any>(&mut self) -> Option<&mut T> {
if self.writer_type_id == std::any::TypeId::of::<T>() {
Some(unsafe { &mut *(self.real_writer as *mut T) })
} else {
None
}
}
fn emit_messages_default(
&mut self,
level: &Level,
msgs: &[(DiagMsg, Style)],
code: &Option<DiagId>,
msp: &MultiSpan,
children: &[SubDiagnostic],
suggestions: &Suggestions,
) {
let renderer = &self.renderer;
let annotation_level = annotation_level_for_level(*level);
let mut title = if msgs.iter().any(|(_, style)| style != &Style::NoStyle) {
annotation_level.clone().secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level)))
} else {
annotation_level.clone().primary_title(self.no_style_msgs(msgs))
};
if let Some(c) = code {
title = title.id(c.as_str());
}
let mut report = vec![];
let mut group = Group::with_title(title);
let Some(sm) = self.source_map.as_ref() else {
group = group.elements(children.iter().map(|c| {
let msg = self.no_style_msgs(&c.messages);
let level = annotation_level_for_level(c.level);
level.message(msg)
}));
report.push(group);
if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) {
panic!("failed to emit error: {e}");
}
return;
};
let mut file_ann = collect_annotations(msp, sm);
let primary_span = msp.primary_span().unwrap_or_default();
if !primary_span.is_dummy() {
let primary_lo = sm.lookup_char_pos(primary_span.lo());
if let Ok(pos) = file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) {
file_ann.swap(0, pos);
}
for (file, annotations) in file_ann.into_iter() {
if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
group = group.element(snippet);
}
}
}
for c in children {
let level = annotation_level_for_level(c.level);
let msg = if c.messages.iter().any(|(_, style)| style != &Style::NoStyle) {
Cow::Owned(self.pre_style_msgs(&c.messages, c.level))
} else {
Cow::Owned(self.no_style_msgs(&c.messages))
};
if !c.span.has_primary_spans() && !c.span.has_span_labels() {
group = group.element(level.clone().message(msg));
continue;
}
report.push(std::mem::replace(
&mut group,
Group::with_title(level.clone().secondary_title(msg)),
));
let mut file_ann = collect_annotations(&c.span, sm);
let primary_span = c.span.primary_span().unwrap_or_default();
if !primary_span.is_dummy() {
let primary_lo = sm.lookup_char_pos(primary_span.lo());
if let Ok(pos) =
file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name))
{
file_ann.swap(0, pos);
}
}
for (file, annotations) in file_ann.into_iter() {
if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
group = group.element(snippet);
}
}
}
let suggestions_expected = suggestions
.iter()
.filter(|s| {
matches!(
s.style,
SuggestionStyle::HideCodeInline
| SuggestionStyle::ShowCode
| SuggestionStyle::ShowAlways
)
})
.count();
for suggestion in suggestions.unwrap_tag() {
match suggestion.style {
SuggestionStyle::CompletelyHidden => {
}
SuggestionStyle::HideCodeAlways => {
let msg = self.no_style_msgs(&[(suggestion.msg.to_owned(), Style::HeaderMsg)]);
group = group.element(annotate_snippets::Level::HELP.message(msg));
}
SuggestionStyle::HideCodeInline
| SuggestionStyle::ShowCode
| SuggestionStyle::ShowAlways => {
let substitutions = suggestion
.substitutions
.iter()
.cloned() .filter_map(|mut subst| {
let invalid =
subst.parts.iter().any(|item| sm.is_valid_span(item.span).is_err());
if invalid {
debug!("suggestion contains an invalid span: {:?}", subst);
}
subst.parts.sort_by_key(|part| part.span.lo());
assert_eq!(
subst.parts.windows(2).find(|s| s[0].span.overlaps(s[1].span)),
None,
"all spans must be disjoint",
);
subst.parts.retain(|p| is_different(sm, &p.snippet, p.span));
if !invalid { Some(subst) } else { None }
})
.collect::<Vec<_>>();
if substitutions.is_empty() {
continue;
}
let mut msg = suggestion.msg.to_string();
let lo = substitutions
.iter()
.find_map(|sub| sub.parts.first().map(|p| p.span.lo()))
.unwrap();
let file = sm.lookup_source_file(lo);
let filename = sm.filename_for_diagnostics(&file.name).to_string();
let other_suggestions = substitutions.len().saturating_sub(MAX_SUGGESTIONS);
let subs = substitutions
.into_iter()
.take(MAX_SUGGESTIONS)
.filter_map(|sub| {
let mut confusion_type = ConfusionType::None;
for part in &sub.parts {
let part_confusion =
detect_confusion_type(sm, &part.snippet, part.span);
confusion_type = confusion_type.combine(part_confusion);
}
if !matches!(confusion_type, ConfusionType::None) {
msg.push_str(confusion_type.label_text());
}
let parts = sub
.parts
.into_iter()
.filter_map(|p| {
if is_different(sm, &p.snippet, p.span) {
Some((p.span, p.snippet))
} else {
None
}
})
.collect::<Vec<_>>();
if parts.is_empty() {
None
} else {
let spans = parts.iter().map(|(span, _)| *span).collect::<Vec<_>>();
let fold = true;
if let Some((bounding_span, source, line_offset)) =
shrink_file(spans.as_slice(), &file.name, sm)
{
let adj_lo = bounding_span.lo().to_usize();
Some(
Snippet::source(source)
.line_start(line_offset)
.path(filename.clone())
.fold(fold)
.patches(parts.into_iter().map(
|(span, replacement)| {
let lo =
span.lo().to_usize().saturating_sub(adj_lo);
let hi =
span.hi().to_usize().saturating_sub(adj_lo);
Patch::new(lo..hi, replacement.into_inner())
},
)),
)
} else {
None
}
}
})
.collect::<Vec<_>>();
if !subs.is_empty() {
report.push(std::mem::replace(
&mut group,
Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)),
));
group = group.elements(subs);
if other_suggestions > 0 {
group = group.element(
annotate_snippets::Level::NOTE.no_name().message(format!(
"and {} other candidate{}",
other_suggestions,
pluralize!(other_suggestions)
)),
);
}
}
}
}
}
if suggestions_expected > 0 && report.is_empty() {
group = group.element(Padding);
}
if !group.is_empty() {
report.push(group);
}
if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) {
panic!("failed to emit error: {e}");
}
}
fn pre_style_msgs(&self, msgs: &[(DiagMsg, Style)], level: Level) -> String {
msgs.iter()
.filter_map(|(m, style)| {
let text = m.as_str();
let style = style.to_color_spec(level);
if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) }
})
.collect()
}
fn no_style_msgs(&self, msgs: &[(DiagMsg, Style)]) -> String {
msgs.iter().map(|(m, _)| m.as_str()).collect()
}
fn annotated_snippet<'a>(
&self,
annotations: Vec<Annotation>,
file_name: &FileName,
sm: &Arc<SourceMap>,
) -> Option<Snippet<'a, ASAnnotation<'a>>> {
let spans = annotations.iter().map(|a| a.span).collect::<Vec<_>>();
if let Some((bounding_span, source, offset_line)) = shrink_file(&spans, file_name, sm) {
let adj_lo = bounding_span.lo().to_usize();
let filename = sm.filename_for_diagnostics(file_name).to_string();
Some(Snippet::source(source).line_start(offset_line).path(filename).annotations(
annotations.into_iter().map(move |a| {
let lo = a.span.lo().to_usize().saturating_sub(adj_lo);
let hi = a.span.hi().to_usize().saturating_sub(adj_lo);
let ann = a.kind.span(lo..hi);
if let Some(label) = a.label { ann.label(label) } else { ann }
}),
))
} else {
None
}
}
}
pub struct HumanBufferEmitter {
inner: HumanEmitter,
}
impl Emitter for HumanBufferEmitter {
#[inline]
fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
self.inner.emit_diagnostic(diagnostic);
}
#[inline]
fn emit_diagnostic_ref(&mut self, diagnostic: &Diag) {
self.inner.emit_diagnostic_ref(diagnostic);
}
#[inline]
fn source_map(&self) -> Option<&Arc<SourceMap>> {
Emitter::source_map(&self.inner)
}
#[inline]
fn supports_color(&self) -> bool {
self.inner.supports_color()
}
}
impl HumanBufferEmitter {
pub fn new(color_choice: ColorChoice) -> Self {
Self { inner: HumanEmitter::new(Vec::<u8>::new(), stderr_choice(color_choice)) }
}
pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
self.inner = self.inner.source_map(source_map);
self
}
pub fn ui_testing(mut self, yes: bool) -> Self {
self.inner = self.inner.ui_testing(yes);
self
}
pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
self.inner = self.inner.human_kind(kind);
self
}
pub fn terminal_width(mut self, width: Option<usize>) -> Self {
self.inner = self.inner.terminal_width(width);
self
}
pub fn inner(&self) -> &HumanEmitter {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut HumanEmitter {
&mut self.inner
}
pub fn buffer(&self) -> &str {
let buffer = self.inner.downcast_writer::<Vec<u8>>().unwrap();
debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
unsafe { std::str::from_utf8_unchecked(buffer) }
}
pub fn buffer_mut(&mut self) -> &mut String {
let buffer = self.inner.downcast_writer_mut::<Vec<u8>>().unwrap();
debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
unsafe { &mut *(buffer as *mut Vec<u8> as *mut String) }
}
}
fn annotation_level_for_level<'a>(level: Level) -> ASLevel<'a> {
match level {
Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => ASLevel::ERROR,
Level::Warning => ASLevel::WARNING,
Level::Note | Level::OnceNote => ASLevel::NOTE,
Level::Help | Level::OnceHelp => ASLevel::HELP,
Level::Allow => ASLevel::INFO,
}
.with_name(if level == Level::FailureNote { None } else { Some(level.to_str()) })
}
fn stderr_choice(color_choice: ColorChoice) -> ColorChoice {
static AUTO: OnceLock<ColorChoice> = OnceLock::new();
if color_choice == ColorChoice::Auto {
*AUTO.get_or_init(|| anstream::AutoStream::choice(&std::io::stderr()))
} else {
color_choice
}
}
fn emit_to_destination(rendered: String, lvl: &Level, dst: &mut Writer) -> io::Result<()> {
writeln!(dst, "{rendered}")?;
if !lvl.is_failure_note() {
writeln!(dst)?;
}
dst.flush()?;
Ok(())
}
#[derive(Debug)]
struct Annotation {
kind: AnnotationKind,
span: Span,
label: Option<String>,
}
fn collect_annotations(
msp: &MultiSpan,
sm: &Arc<SourceMap>,
) -> Vec<(Arc<SourceFile>, Vec<Annotation>)> {
let mut output: Vec<(Arc<SourceFile>, Vec<Annotation>)> = vec![];
for SpanLabel { span, is_primary, label } in msp.span_labels() {
let span = match (span.is_dummy(), msp.primary_span()) {
(_, None) | (false, _) => span,
(true, Some(span)) => span,
};
let file = sm.lookup_source_file(span.lo());
let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context };
let label = label.as_ref().map(|m| normalize_whitespace(m));
let ann = Annotation { kind, span, label };
if sm.is_valid_span(ann.span).is_ok() {
if let Some((_, annotations)) =
output.iter_mut().find(|(f, _)| f.start_pos == file.start_pos)
{
annotations.push(ann);
} else {
output.push((file, vec![ann]));
}
}
}
for (_, ann) in output.iter_mut() {
ann.sort_by_key(|a| {
let lo = sm.lookup_char_pos(a.span.lo());
lo.line
});
}
output
}
fn shrink_file(
spans: &[Span],
_file_name: &FileName,
sm: &Arc<SourceMap>,
) -> Option<(Span, String, usize)> {
let lo_byte = spans.iter().map(|s| s.lo()).min()?;
let lo_loc = sm.lookup_char_pos(lo_byte);
let hi_byte = spans.iter().map(|s| s.hi()).max()?;
let hi_loc = sm.lookup_char_pos(hi_byte);
if lo_loc.file.start_pos != hi_loc.file.start_pos {
return None;
}
let lo = lo_loc.file.line_bounds(lo_loc.line.saturating_sub(1)).start;
let hi = lo_loc.file.line_bounds(hi_loc.line.saturating_sub(1)).end;
let bounding_span = Span::new(lo, hi);
let source = sm.span_to_snippet(bounding_span).ok()?;
let offset_line = lo_loc.line;
Some((bounding_span, source, offset_line))
}