solar_interface/diagnostics/emitter/
human.rs1use super::{Diag, Emitter};
2use crate::{
3 SourceMap, Span,
4 diagnostics::{
5 ConfusionType, DiagId, DiagMsg, Level, MultiSpan, SpanLabel, Style, SubDiagnostic,
6 SuggestionStyle, Suggestions, detect_confusion_type, emitter::normalize_whitespace,
7 is_different,
8 },
9 source_map::{FileName, SourceFile},
10};
11use annotate_snippets::{
12 Annotation as ASAnnotation, AnnotationKind, Group, Level as ASLevel, Padding, Patch, Renderer,
13 Snippet, renderer::DecorStyle,
14};
15use anstream::{AutoStream, ColorChoice};
16use solar_config::HumanEmitterKind;
17use solar_data_structures::pluralize;
18use std::{
19 any::Any,
20 borrow::Cow,
21 io::{self, Write},
22 sync::{Arc, OnceLock},
23};
24
25type Writer = dyn Write + Send + 'static;
26
27pub(super) const MAX_SUGGESTIONS: usize = 4;
31
32const DEFAULT_RENDERER: Renderer = Renderer::styled()
33 .error(Level::Error.style())
34 .warning(Level::Warning.style())
35 .note(Level::Note.style())
36 .help(Level::Help.style())
37 .line_num(Style::LineNumber.to_color_spec(Level::Note))
38 .addition(Style::Addition.to_color_spec(Level::Note))
39 .removal(Style::Removal.to_color_spec(Level::Note))
40 .context(Style::LabelSecondary.to_color_spec(Level::Note));
41
42pub struct HumanEmitter {
44 writer_type_id: std::any::TypeId,
45 real_writer: *mut Writer,
46 writer: AutoStream<Box<Writer>>,
47 source_map: Option<Arc<SourceMap>>,
48 renderer: Renderer,
49}
50
51unsafe impl Send for HumanEmitter {}
53
54impl Emitter for HumanEmitter {
55 fn emit_diagnostic(&mut self, diag: &mut Diag) {
56 let mut primary_span = Cow::Owned(std::mem::take(&mut diag.span));
57 let mut suggestions = Cow::Owned(std::mem::take(&mut diag.suggestions));
58 self.primary_span_formatted(&mut primary_span, &mut suggestions);
59
60 self.emit_messages_default(
61 &diag.level,
62 &diag.messages,
63 &diag.code,
64 &primary_span,
65 &diag.children,
66 &suggestions,
67 );
68
69 diag.span = primary_span.into_owned();
70 diag.suggestions = suggestions.into_owned();
71 }
72
73 fn emit_diagnostic_ref(&mut self, diag: &Diag) {
74 let mut primary_span = Cow::Borrowed(&diag.span);
75 let mut suggestions = Cow::Borrowed(&diag.suggestions);
76 self.primary_span_formatted(&mut primary_span, &mut suggestions);
77
78 self.emit_messages_default(
79 &diag.level,
80 &diag.messages,
81 &diag.code,
82 &primary_span,
83 &diag.children,
84 &suggestions,
85 );
86 }
87
88 fn source_map(&self) -> Option<&Arc<SourceMap>> {
89 self.source_map.as_ref()
90 }
91
92 fn supports_color(&self) -> bool {
93 match self.writer.current_choice() {
94 ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
95 ColorChoice::Auto | ColorChoice::Never => false,
96 }
97 }
98}
99
100impl HumanEmitter {
101 pub fn new<W: Write + Send + 'static>(writer: W, color: ColorChoice) -> Self {
107 let writer_type_id = writer.type_id();
108 let mut real_writer = Box::new(writer) as Box<Writer>;
109 Self {
110 writer_type_id,
111 real_writer: &mut *real_writer,
112 writer: AutoStream::new(real_writer, color),
113 source_map: None,
114 renderer: DEFAULT_RENDERER,
115 }
116 .human_kind(Default::default())
117 }
118
119 pub fn test() -> Self {
121 struct TestWriter;
122
123 impl Write for TestWriter {
124 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
125 eprint!("{}", String::from_utf8_lossy(buf));
128 Ok(buf.len())
129 }
130
131 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
132 self.write(buf).map(drop)
133 }
134
135 fn flush(&mut self) -> io::Result<()> {
136 io::stderr().flush()
137 }
138 }
139
140 Self::new(TestWriter, ColorChoice::Always)
141 }
142
143 pub fn stderr(color_choice: ColorChoice) -> Self {
145 Self::new(io::BufWriter::new(io::stderr()), stderr_choice(color_choice))
147 }
148
149 pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
151 self.set_source_map(source_map);
152 self
153 }
154
155 pub fn set_source_map(&mut self, source_map: Option<Arc<SourceMap>>) {
157 self.source_map = source_map;
158 }
159
160 pub fn ui_testing(mut self, yes: bool) -> Self {
162 self.renderer = self.renderer.anonymized_line_numbers(yes);
163 self
164 }
165
166 pub fn set_ui_testing(&mut self, yes: bool) {
168 self.renderer =
169 std::mem::replace(&mut self.renderer, DEFAULT_RENDERER).anonymized_line_numbers(yes);
170 }
171
172 pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
174 match kind {
175 HumanEmitterKind::Ascii => {
176 self.renderer = self.renderer.decor_style(DecorStyle::Ascii);
177 }
178 HumanEmitterKind::Unicode => {
179 self.renderer = self.renderer.decor_style(DecorStyle::Unicode);
180 }
181 HumanEmitterKind::Short => {
182 self.renderer = self.renderer.short_message(true);
183 }
184 _ => unimplemented!("{kind:?}"),
185 }
186 self
187 }
188
189 pub fn terminal_width(mut self, width: Option<usize>) -> Self {
191 if let Some(w) = width {
192 self.renderer = self.renderer.term_width(w);
193 }
194 self
195 }
196
197 fn downcast_writer<T: Any>(&self) -> Option<&T> {
199 if self.writer_type_id == std::any::TypeId::of::<T>() {
200 Some(unsafe { &*(self.real_writer as *const T) })
201 } else {
202 None
203 }
204 }
205
206 fn downcast_writer_mut<T: Any>(&mut self) -> Option<&mut T> {
208 if self.writer_type_id == std::any::TypeId::of::<T>() {
209 Some(unsafe { &mut *(self.real_writer as *mut T) })
210 } else {
211 None
212 }
213 }
214
215 fn emit_messages_default(
216 &mut self,
217 level: &Level,
218 msgs: &[(DiagMsg, Style)],
219 code: &Option<DiagId>,
220 msp: &MultiSpan,
221 children: &[SubDiagnostic],
222 suggestions: &Suggestions,
223 ) {
224 let renderer = &self.renderer;
225 let annotation_level = annotation_level_for_level(*level);
226
227 let mut title = if msgs.iter().any(|(_, style)| style != &Style::NoStyle) {
230 annotation_level.clone().secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level)))
231 } else {
232 annotation_level.clone().primary_title(self.no_style_msgs(msgs))
233 };
234
235 if let Some(c) = code {
236 title = title.id(c.as_str());
237 }
243
244 let mut report = vec![];
245 let mut group = Group::with_title(title);
246
247 let Some(sm) = self.source_map.as_ref() else {
249 group = group.elements(children.iter().map(|c| {
250 let msg = self.no_style_msgs(&c.messages);
251 let level = annotation_level_for_level(c.level);
252 level.message(msg)
253 }));
254
255 report.push(group);
256 if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) {
257 panic!("failed to emit error: {e}");
258 }
259 return;
260 };
261
262 let mut file_ann = collect_annotations(msp, sm);
263
264 let primary_span = msp.primary_span().unwrap_or_default();
266 if !primary_span.is_dummy() {
267 let primary_lo = sm.lookup_char_pos(primary_span.lo());
268 if let Ok(pos) = file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) {
269 file_ann.swap(0, pos);
270 }
271
272 for (file, annotations) in file_ann.into_iter() {
273 if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
274 group = group.element(snippet);
275 }
276 }
277 }
278
279 for c in children {
280 let level = annotation_level_for_level(c.level);
281
282 let msg = if c.messages.iter().any(|(_, style)| style != &Style::NoStyle) {
285 Cow::Owned(self.pre_style_msgs(&c.messages, c.level))
286 } else {
287 Cow::Owned(self.no_style_msgs(&c.messages))
288 };
289
290 if !c.span.has_primary_spans() && !c.span.has_span_labels() {
292 group = group.element(level.clone().message(msg));
293 continue;
294 }
295
296 report.push(std::mem::replace(
297 &mut group,
298 Group::with_title(level.clone().secondary_title(msg)),
299 ));
300
301 let mut file_ann = collect_annotations(&c.span, sm);
302 let primary_span = c.span.primary_span().unwrap_or_default();
303 if !primary_span.is_dummy() {
304 let primary_lo = sm.lookup_char_pos(primary_span.lo());
305 if let Ok(pos) =
306 file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name))
307 {
308 file_ann.swap(0, pos);
309 }
310 }
311
312 for (file, annotations) in file_ann.into_iter() {
313 if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
314 group = group.element(snippet);
315 }
316 }
317 }
318
319 let suggestions_expected = suggestions
320 .iter()
321 .filter(|s| {
322 matches!(
323 s.style,
324 SuggestionStyle::HideCodeInline
325 | SuggestionStyle::ShowCode
326 | SuggestionStyle::ShowAlways
327 )
328 })
329 .count();
330 for suggestion in suggestions.unwrap_tag() {
331 match suggestion.style {
332 SuggestionStyle::CompletelyHidden => {
333 }
335 SuggestionStyle::HideCodeAlways => {
336 let msg = self.no_style_msgs(&[(suggestion.msg.to_owned(), Style::HeaderMsg)]);
337 group = group.element(annotate_snippets::Level::HELP.message(msg));
338 }
339 SuggestionStyle::HideCodeInline
340 | SuggestionStyle::ShowCode
341 | SuggestionStyle::ShowAlways => {
342 let substitutions = suggestion
343 .substitutions
344 .iter()
345 .cloned() .filter_map(|mut subst| {
347 let invalid =
351 subst.parts.iter().any(|item| sm.is_valid_span(item.span).is_err());
352 if invalid {
353 debug!("suggestion contains an invalid span: {:?}", subst);
354 }
355
356 subst.parts.sort_by_key(|part| part.span.lo());
359 assert_eq!(
361 subst.parts.windows(2).find(|s| s[0].span.overlaps(s[1].span)),
362 None,
363 "all spans must be disjoint",
364 );
365
366 subst.parts.retain(|p| is_different(sm, &p.snippet, p.span));
371
372 if !invalid { Some(subst) } else { None }
373 })
374 .collect::<Vec<_>>();
375
376 if substitutions.is_empty() {
377 continue;
378 }
379 let mut msg = suggestion.msg.to_string();
380
381 let lo = substitutions
382 .iter()
383 .find_map(|sub| sub.parts.first().map(|p| p.span.lo()))
384 .unwrap();
385 let file = sm.lookup_source_file(lo);
386
387 let filename = sm.filename_for_diagnostics(&file.name).to_string();
388
389 let other_suggestions = substitutions.len().saturating_sub(MAX_SUGGESTIONS);
390
391 let subs = substitutions
392 .into_iter()
393 .take(MAX_SUGGESTIONS)
394 .filter_map(|sub| {
395 let mut confusion_type = ConfusionType::None;
396 for part in &sub.parts {
397 let part_confusion =
398 detect_confusion_type(sm, &part.snippet, part.span);
399 confusion_type = confusion_type.combine(part_confusion);
400 }
401
402 if !matches!(confusion_type, ConfusionType::None) {
403 msg.push_str(confusion_type.label_text());
404 }
405
406 let parts = sub
407 .parts
408 .into_iter()
409 .filter_map(|p| {
410 if is_different(sm, &p.snippet, p.span) {
411 Some((p.span, p.snippet))
412 } else {
413 None
414 }
415 })
416 .collect::<Vec<_>>();
417
418 if parts.is_empty() {
419 None
420 } else {
421 let spans = parts.iter().map(|(span, _)| *span).collect::<Vec<_>>();
422
423 let fold = true;
427
428 if let Some((bounding_span, source, line_offset)) =
429 shrink_file(spans.as_slice(), &file.name, sm)
430 {
431 let adj_lo = bounding_span.lo().to_usize();
432 Some(
433 Snippet::source(source)
434 .line_start(line_offset)
435 .path(filename.clone())
436 .fold(fold)
437 .patches(parts.into_iter().map(
438 |(span, replacement)| {
439 let lo =
440 span.lo().to_usize().saturating_sub(adj_lo);
441 let hi =
442 span.hi().to_usize().saturating_sub(adj_lo);
443
444 Patch::new(lo..hi, replacement.into_inner())
445 },
446 )),
447 )
448 } else {
449 None
450 }
451 }
452 })
453 .collect::<Vec<_>>();
454 if !subs.is_empty() {
455 report.push(std::mem::replace(
456 &mut group,
457 Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)),
458 ));
459
460 group = group.elements(subs);
461 if other_suggestions > 0 {
462 group = group.element(
463 annotate_snippets::Level::NOTE.no_name().message(format!(
464 "and {} other candidate{}",
465 other_suggestions,
466 pluralize!(other_suggestions)
467 )),
468 );
469 }
470 }
471 }
472 }
473 }
474
475 if suggestions_expected > 0 && report.is_empty() {
478 group = group.element(Padding);
479 }
480
481 if !group.is_empty() {
482 report.push(group);
483 }
484
485 if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) {
486 panic!("failed to emit error: {e}");
487 }
488 }
489
490 fn pre_style_msgs(&self, msgs: &[(DiagMsg, Style)], level: Level) -> String {
491 msgs.iter()
492 .filter_map(|(m, style)| {
493 let text = m.as_str();
494 let style = style.to_color_spec(level);
495 if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) }
496 })
497 .collect()
498 }
499
500 fn no_style_msgs(&self, msgs: &[(DiagMsg, Style)]) -> String {
504 msgs.iter().map(|(m, _)| m.as_str()).collect()
505 }
506
507 fn annotated_snippet<'a>(
508 &self,
509 annotations: Vec<Annotation>,
510 file_name: &FileName,
511 sm: &Arc<SourceMap>,
512 ) -> Option<Snippet<'a, ASAnnotation<'a>>> {
513 let spans = annotations.iter().map(|a| a.span).collect::<Vec<_>>();
514 if let Some((bounding_span, source, offset_line)) = shrink_file(&spans, file_name, sm) {
515 let adj_lo = bounding_span.lo().to_usize();
516
517 let filename = sm.filename_for_diagnostics(file_name).to_string();
518
519 Some(Snippet::source(source).line_start(offset_line).path(filename).annotations(
520 annotations.into_iter().map(move |a| {
521 let lo = a.span.lo().to_usize().saturating_sub(adj_lo);
522 let hi = a.span.hi().to_usize().saturating_sub(adj_lo);
523 let ann = a.kind.span(lo..hi);
524 if let Some(label) = a.label { ann.label(label) } else { ann }
525 }),
526 ))
527 } else {
528 None
529 }
530 }
531}
532
533pub struct HumanBufferEmitter {
535 inner: HumanEmitter,
536}
537
538impl Emitter for HumanBufferEmitter {
539 #[inline]
540 fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
541 self.inner.emit_diagnostic(diagnostic);
542 }
543
544 #[inline]
545 fn emit_diagnostic_ref(&mut self, diagnostic: &Diag) {
546 self.inner.emit_diagnostic_ref(diagnostic);
547 }
548
549 #[inline]
550 fn source_map(&self) -> Option<&Arc<SourceMap>> {
551 Emitter::source_map(&self.inner)
552 }
553
554 #[inline]
555 fn supports_color(&self) -> bool {
556 self.inner.supports_color()
557 }
558}
559
560impl HumanBufferEmitter {
561 pub fn new(color_choice: ColorChoice) -> Self {
563 Self { inner: HumanEmitter::new(Vec::<u8>::new(), stderr_choice(color_choice)) }
564 }
565
566 pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
568 self.inner = self.inner.source_map(source_map);
569 self
570 }
571
572 pub fn ui_testing(mut self, yes: bool) -> Self {
574 self.inner = self.inner.ui_testing(yes);
575 self
576 }
577
578 pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
580 self.inner = self.inner.human_kind(kind);
581 self
582 }
583
584 pub fn terminal_width(mut self, width: Option<usize>) -> Self {
586 self.inner = self.inner.terminal_width(width);
587 self
588 }
589
590 pub fn inner(&self) -> &HumanEmitter {
592 &self.inner
593 }
594
595 pub fn inner_mut(&mut self) -> &mut HumanEmitter {
597 &mut self.inner
598 }
599
600 pub fn buffer(&self) -> &str {
602 let buffer = self.inner.downcast_writer::<Vec<u8>>().unwrap();
603 debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
604 unsafe { std::str::from_utf8_unchecked(buffer) }
606 }
607
608 pub fn buffer_mut(&mut self) -> &mut String {
610 let buffer = self.inner.downcast_writer_mut::<Vec<u8>>().unwrap();
611 debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
612 unsafe { &mut *(buffer as *mut Vec<u8> as *mut String) }
614 }
615}
616
617fn annotation_level_for_level<'a>(level: Level) -> ASLevel<'a> {
618 match level {
619 Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => ASLevel::ERROR,
620 Level::Warning => ASLevel::WARNING,
621 Level::Note | Level::OnceNote => ASLevel::NOTE,
622 Level::Help | Level::OnceHelp => ASLevel::HELP,
623 Level::Allow => ASLevel::INFO,
624 }
625 .with_name(if level == Level::FailureNote { None } else { Some(level.to_str()) })
626}
627
628fn stderr_choice(color_choice: ColorChoice) -> ColorChoice {
629 static AUTO: OnceLock<ColorChoice> = OnceLock::new();
630 if color_choice == ColorChoice::Auto {
631 *AUTO.get_or_init(|| anstream::AutoStream::choice(&std::io::stderr()))
632 } else {
633 color_choice
634 }
635}
636
637fn emit_to_destination(rendered: String, lvl: &Level, dst: &mut Writer) -> io::Result<()> {
638 writeln!(dst, "{rendered}")?;
643 if !lvl.is_failure_note() {
644 writeln!(dst)?;
645 }
646 dst.flush()?;
647 Ok(())
648}
649
650#[derive(Debug)]
651struct Annotation {
652 kind: AnnotationKind,
653 span: Span,
654 label: Option<String>,
655}
656
657fn collect_annotations(
658 msp: &MultiSpan,
659 sm: &Arc<SourceMap>,
660) -> Vec<(Arc<SourceFile>, Vec<Annotation>)> {
661 let mut output: Vec<(Arc<SourceFile>, Vec<Annotation>)> = vec![];
662
663 for SpanLabel { span, is_primary, label } in msp.span_labels() {
664 let span = match (span.is_dummy(), msp.primary_span()) {
667 (_, None) | (false, _) => span,
668 (true, Some(span)) => span,
669 };
670 let file = sm.lookup_source_file(span.lo());
671
672 let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context };
673
674 let label = label.as_ref().map(|m| normalize_whitespace(m));
675
676 let ann = Annotation { kind, span, label };
677 if sm.is_valid_span(ann.span).is_ok() {
678 if let Some((_, annotations)) =
680 output.iter_mut().find(|(f, _)| f.start_pos == file.start_pos)
681 {
682 annotations.push(ann);
683 } else {
684 output.push((file, vec![ann]));
685 }
686 }
687 }
688
689 for (_, ann) in output.iter_mut() {
691 ann.sort_by_key(|a| {
692 let lo = sm.lookup_char_pos(a.span.lo());
693 lo.line
694 });
695 }
696
697 output
698}
699
700fn shrink_file(
701 spans: &[Span],
702 _file_name: &FileName,
703 sm: &Arc<SourceMap>,
704) -> Option<(Span, String, usize)> {
705 let lo_byte = spans.iter().map(|s| s.lo()).min()?;
706 let lo_loc = sm.lookup_char_pos(lo_byte);
707
708 let hi_byte = spans.iter().map(|s| s.hi()).max()?;
709 let hi_loc = sm.lookup_char_pos(hi_byte);
710
711 if lo_loc.file.start_pos != hi_loc.file.start_pos {
712 return None;
714 }
715
716 let lo = lo_loc.file.line_bounds(lo_loc.line.saturating_sub(1)).start;
717 let hi = lo_loc.file.line_bounds(hi_loc.line.saturating_sub(1)).end;
718
719 let bounding_span = Span::new(lo, hi);
720 let source = sm.span_to_snippet(bounding_span).ok()?;
721 let offset_line = lo_loc.line;
722
723 Some((bounding_span, source, offset_line))
724}