solar_interface/diagnostics/emitter/
human.rs1use super::{Diag, Emitter, io_panic, rustc::FileWithAnnotatedLines};
2use crate::{
3 SourceMap,
4 diagnostics::{Level, MultiSpan, Style, SubDiagnostic, SuggestionStyle},
5 source_map::SourceFile,
6};
7use annotate_snippets::{
8 Annotation, AnnotationKind, Group, Level as ASLevel, Message, Patch, Renderer, Report, Snippet,
9 Title, renderer::DecorStyle,
10};
11use anstream::{AutoStream, ColorChoice};
12use solar_config::HumanEmitterKind;
13use std::{
14 any::Any,
15 borrow::Cow,
16 collections::BTreeMap,
17 io::{self, Write},
18 sync::{Arc, OnceLock},
19};
20
21type Writer = dyn Write + Send + 'static;
24
25const DEFAULT_RENDERER: Renderer = Renderer::styled()
26 .error(Level::Error.style())
27 .warning(Level::Warning.style())
28 .note(Level::Note.style())
29 .help(Level::Help.style())
30 .line_num(Style::LineNumber.to_color_spec(Level::Note))
31 .addition(Style::Addition.to_color_spec(Level::Note))
32 .removal(Style::Removal.to_color_spec(Level::Note))
33 .context(Style::LabelSecondary.to_color_spec(Level::Note));
34
35pub struct HumanEmitter {
37 writer_type_id: std::any::TypeId,
38 real_writer: *mut Writer,
39 writer: AutoStream<Box<Writer>>,
40 source_map: Option<Arc<SourceMap>>,
41 renderer: Renderer,
42}
43
44unsafe impl Send for HumanEmitter {}
46
47impl Emitter for HumanEmitter {
48 fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
49 self.snippet(diagnostic, |this, snippet| {
50 writeln!(this.writer, "{}\n", this.renderer.render(snippet))?;
51 this.writer.flush()
52 })
53 .unwrap_or_else(|e| io_panic(e));
54 }
55
56 fn source_map(&self) -> Option<&Arc<SourceMap>> {
57 self.source_map.as_ref()
58 }
59
60 fn supports_color(&self) -> bool {
61 match self.writer.current_choice() {
62 ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
63 ColorChoice::Auto | ColorChoice::Never => false,
64 }
65 }
66}
67
68impl HumanEmitter {
69 pub fn new<W: Write + Send + 'static>(writer: W, color: ColorChoice) -> Self {
75 let writer_type_id = writer.type_id();
76 let mut real_writer = Box::new(writer) as Box<Writer>;
77 Self {
78 writer_type_id,
79 real_writer: &mut *real_writer,
80 writer: AutoStream::new(real_writer, color),
81 source_map: None,
82 renderer: DEFAULT_RENDERER,
83 }
84 }
85
86 pub fn test() -> Self {
88 struct TestWriter;
89
90 impl Write for TestWriter {
91 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
92 eprint!("{}", String::from_utf8_lossy(buf));
95 Ok(buf.len())
96 }
97
98 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
99 self.write(buf).map(drop)
100 }
101
102 fn flush(&mut self) -> io::Result<()> {
103 io::stderr().flush()
104 }
105 }
106
107 Self::new(TestWriter, ColorChoice::Always)
108 }
109
110 pub fn stderr(color_choice: ColorChoice) -> Self {
112 Self::new(io::BufWriter::new(io::stderr()), stderr_choice(color_choice))
114 }
115
116 pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
118 self.set_source_map(source_map);
119 self
120 }
121
122 pub fn set_source_map(&mut self, source_map: Option<Arc<SourceMap>>) {
124 self.source_map = source_map;
125 }
126
127 pub fn ui_testing(mut self, yes: bool) -> Self {
129 self.renderer = self.renderer.anonymized_line_numbers(yes);
130 self
131 }
132
133 pub fn set_ui_testing(&mut self, yes: bool) {
135 self.renderer =
136 std::mem::replace(&mut self.renderer, DEFAULT_RENDERER).anonymized_line_numbers(yes);
137 }
138
139 pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
141 match kind {
142 HumanEmitterKind::Ascii => {
143 self.renderer = self.renderer.decor_style(DecorStyle::Ascii);
144 }
145 HumanEmitterKind::Unicode => {
146 self.renderer = self.renderer.decor_style(DecorStyle::Unicode);
147 }
148 HumanEmitterKind::Short => {
149 self.renderer = self.renderer.short_message(true);
150 }
151 _ => unimplemented!("{kind:?}"),
152 }
153 self
154 }
155
156 pub fn terminal_width(mut self, width: Option<usize>) -> Self {
158 if let Some(w) = width {
159 self.renderer = self.renderer.term_width(w);
160 }
161 self
162 }
163
164 fn downcast_writer<T: Any>(&self) -> Option<&T> {
166 if self.writer_type_id == std::any::TypeId::of::<T>() {
167 Some(unsafe { &*(self.real_writer as *const T) })
168 } else {
169 None
170 }
171 }
172
173 fn downcast_writer_mut<T: Any>(&mut self) -> Option<&mut T> {
175 if self.writer_type_id == std::any::TypeId::of::<T>() {
176 Some(unsafe { &mut *(self.real_writer as *mut T) })
177 } else {
178 None
179 }
180 }
181
182 fn snippet<R>(
184 &mut self,
185 diagnostic: &mut Diag,
186 f: impl FnOnce(&mut Self, Report<'_>) -> R,
187 ) -> R {
188 let mut primary_span = Cow::Borrowed(&diagnostic.span);
210 self.primary_span_formatted(&mut primary_span, &mut diagnostic.suggestions);
211
212 let children = diagnostic
215 .suggestions
216 .iter()
217 .filter(|sugg| sugg.style != SuggestionStyle::HideCodeAlways)
218 .collect::<Vec<_>>();
219
220 let sm = self.source_map.as_deref();
221 let title = title_from_diagnostic(diagnostic);
222 let snippets = sm.map(|sm| iter_snippets(sm, &primary_span)).into_iter().flatten();
223
224 let subs = |d| diagnostic.children.iter().filter(move |sub| sub.span.is_dummy() == d);
226 let sub_groups = subs(false).map(|sub| {
227 let mut g = Group::with_title(title_from_subdiagnostic(sub, self.supports_color()));
228 if let Some(sm) = sm {
229 g = g.elements(iter_snippets(sm, &sub.span));
230 }
231 g
232 });
233
234 let mut footers =
235 subs(true).map(|sub| message_from_subdiagnostic(sub, self.supports_color())).peekable();
236 let footer_group =
237 footers.peek().is_some().then(|| Group::with_level(ASLevel::NOTE).elements(footers));
238
239 let suggestion_groups = children.iter().flat_map(|suggestion| {
241 let sm = self.source_map.as_deref()?;
242
243 for substitution in &suggestion.substitutions {
246 let mut parts_by_file: BTreeMap<_, Vec<_>> = BTreeMap::new();
248 for part in &substitution.parts {
249 let file = sm.lookup_source_file(part.span.lo());
250 parts_by_file.entry(file.name.clone()).or_default().push(part);
251 }
252
253 if parts_by_file.is_empty() {
254 continue;
255 }
256
257 let mut snippets = vec![];
258 for (filename, parts) in parts_by_file {
259 let file = sm.get_file_ref(&filename)?;
260 let mut snippet = Snippet::source(file.src.to_string())
261 .path(sm.filename_for_diagnostics(&file.name).to_string())
262 .fold(true);
263
264 for part in parts {
265 if let Ok(range) = sm.span_to_range(part.span) {
266 snippet = snippet.patch(Patch::new(range, part.snippet.as_str()));
267 }
268 }
269 snippets.push(snippet);
270 }
271
272 if !snippets.is_empty() {
273 let title = ASLevel::HELP.secondary_title(suggestion.msg.as_str());
274 return Some(Group::with_title(title).elements(snippets));
275 }
276 }
277
278 None
279 });
280
281 let main_group = Group::with_title(title).elements(snippets);
282 let report = std::iter::once(main_group)
283 .chain(suggestion_groups)
284 .chain(footer_group)
285 .chain(sub_groups)
286 .collect::<Vec<_>>();
287 f(self, &report)
288 }
289}
290
291pub struct HumanBufferEmitter {
293 inner: HumanEmitter,
294}
295
296impl Emitter for HumanBufferEmitter {
297 #[inline]
298 fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
299 self.inner.emit_diagnostic(diagnostic);
300 }
301
302 #[inline]
303 fn source_map(&self) -> Option<&Arc<SourceMap>> {
304 Emitter::source_map(&self.inner)
305 }
306
307 #[inline]
308 fn supports_color(&self) -> bool {
309 self.inner.supports_color()
310 }
311}
312
313impl HumanBufferEmitter {
314 pub fn new(color_choice: ColorChoice) -> Self {
316 Self { inner: HumanEmitter::new(Vec::<u8>::new(), stderr_choice(color_choice)) }
317 }
318
319 pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
321 self.inner = self.inner.source_map(source_map);
322 self
323 }
324
325 pub fn ui_testing(mut self, yes: bool) -> Self {
327 self.inner = self.inner.ui_testing(yes);
328 self
329 }
330
331 pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
333 self.inner = self.inner.human_kind(kind);
334 self
335 }
336
337 pub fn terminal_width(mut self, width: Option<usize>) -> Self {
339 self.inner = self.inner.terminal_width(width);
340 self
341 }
342
343 pub fn inner(&self) -> &HumanEmitter {
345 &self.inner
346 }
347
348 pub fn inner_mut(&mut self) -> &mut HumanEmitter {
350 &mut self.inner
351 }
352
353 pub fn buffer(&self) -> &str {
355 let buffer = self.inner.downcast_writer::<Vec<u8>>().unwrap();
356 debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
357 unsafe { std::str::from_utf8_unchecked(buffer) }
359 }
360
361 pub fn buffer_mut(&mut self) -> &mut String {
363 let buffer = self.inner.downcast_writer_mut::<Vec<u8>>().unwrap();
364 debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
365 unsafe { &mut *(buffer as *mut Vec<u8> as *mut String) }
367 }
368}
369
370fn title_from_diagnostic(diag: &Diag) -> Title<'_> {
371 let mut title = to_as_level(diag.level).primary_title(diag.label());
372 if let Some(id) = diag.id() {
373 title = title.id(id);
374 }
375 title
376}
377
378fn title_from_subdiagnostic(sub: &SubDiagnostic, supports_color: bool) -> Title<'_> {
379 to_as_level(sub.level).secondary_title(sub.label_with_style(supports_color))
380}
381
382fn message_from_subdiagnostic(sub: &SubDiagnostic, supports_color: bool) -> Message<'_> {
383 to_as_level(sub.level).message(sub.label_with_style(supports_color))
384}
385
386fn iter_snippets<'a>(
387 sm: &SourceMap,
388 msp: &MultiSpan,
389) -> impl Iterator<Item = Snippet<'a, Annotation<'a>>> {
390 collect_files(sm, msp).into_iter().map(|file| file_to_snippet(sm, &file.file, &file.lines))
391}
392
393fn collect_files(sm: &SourceMap, msp: &MultiSpan) -> Vec<FileWithAnnotatedLines> {
394 let mut annotated_files = FileWithAnnotatedLines::collect_annotations(sm, msp);
395 if let Some(primary_span) = msp.primary_span()
397 && !primary_span.is_dummy()
398 && annotated_files.len() > 1
399 {
400 let primary_lo = sm.lookup_char_pos(primary_span.lo());
401 if let Ok(pos) =
402 annotated_files.binary_search_by(|x| x.file.name.cmp(&primary_lo.file.name))
403 {
404 annotated_files.swap(0, pos);
405 }
406 }
407 annotated_files
408}
409
410fn file_to_snippet<'a>(
415 sm: &SourceMap,
416 file: &SourceFile,
417 lines: &[super::rustc::Line],
418) -> Snippet<'a, Annotation<'a>> {
419 type MultiLine<'a> = (Option<&'a String>, usize);
421 fn multi_line_at<'a, 'b>(
422 mls: &'a mut Vec<MultiLine<'b>>,
423 depth: usize,
424 ) -> &'a mut MultiLine<'b> {
425 assert!(depth > 0);
426 if mls.len() < depth {
427 mls.resize_with(depth, || (None, 0));
428 }
429 &mut mls[depth - 1]
430 }
431
432 debug_assert!(!lines.is_empty());
433
434 let first_line = lines.first().unwrap().line_index;
435 debug_assert!(first_line > 0, "line index is 1-based");
436 let last_line = lines.last().unwrap().line_index;
437 debug_assert!(last_line >= first_line);
438 debug_assert!(lines.is_sorted());
439 let snippet_base = file.line_position(first_line - 1).unwrap();
440
441 let source = file.get_lines(first_line - 1..=last_line - 1).unwrap_or_default();
442 let mut annotations = Vec::new();
443 let mut push_annotation = |kind: AnnotationKind, span, label| {
444 annotations.push(kind.span(span).label(label));
445 };
446 let annotation_kind = |is_primary: bool| {
447 if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context }
448 };
449
450 let mut mls = Vec::new();
451 for line in lines {
452 let line_abs_pos = file.line_position(line.line_index - 1).unwrap();
453 let line_rel_pos = line_abs_pos - snippet_base;
454 let rel_pos = |c: &super::rustc::AnnotationColumn| {
457 line_rel_pos + char_to_byte_pos(&source[line_rel_pos..], c.file)
458 };
459
460 for ann in &line.annotations {
461 match ann.annotation_type {
462 super::rustc::AnnotationType::Singleline => {
463 push_annotation(
464 annotation_kind(ann.is_primary),
465 rel_pos(&ann.start_col)..rel_pos(&ann.end_col),
466 ann.label.clone().unwrap_or_default(),
467 );
468 }
469 super::rustc::AnnotationType::MultilineStart(depth) => {
470 *multi_line_at(&mut mls, depth) = (ann.label.as_ref(), rel_pos(&ann.start_col));
471 }
472 super::rustc::AnnotationType::MultilineLine(_depth) => {
473 push_annotation(
475 AnnotationKind::Visible,
476 line_rel_pos..line_rel_pos,
477 String::new(),
478 );
479 }
480 super::rustc::AnnotationType::MultilineEnd(depth) => {
481 let (label, multiline_start_idx) = *multi_line_at(&mut mls, depth);
482 let end_idx = rel_pos(&ann.end_col);
483 debug_assert!(end_idx >= multiline_start_idx);
484 push_annotation(
485 annotation_kind(ann.is_primary),
486 multiline_start_idx..end_idx,
487 label.or(ann.label.as_ref()).cloned().unwrap_or_default(),
488 );
489 }
490 }
491 }
492 }
493 Snippet::source(source.to_string())
494 .path(sm.filename_for_diagnostics(&file.name).to_string())
495 .line_start(first_line)
496 .fold(true)
497 .annotations(annotations)
498}
499
500fn to_as_level<'a>(level: Level) -> ASLevel<'a> {
501 match level {
502 Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => ASLevel::ERROR,
503 Level::Warning => ASLevel::WARNING,
504 Level::Note | Level::OnceNote => ASLevel::NOTE,
505 Level::Help | Level::OnceHelp => ASLevel::HELP,
506 Level::Allow => ASLevel::INFO,
507 }
508 .with_name(if level == Level::FailureNote { None } else { Some(level.to_str()) })
509}
510
511fn char_to_byte_pos(s: &str, char_pos: usize) -> usize {
512 s.chars().take(char_pos).map(char::len_utf8).sum()
513}
514
515fn stderr_choice(color_choice: ColorChoice) -> ColorChoice {
516 static AUTO: OnceLock<ColorChoice> = OnceLock::new();
517 if color_choice == ColorChoice::Auto {
518 *AUTO.get_or_init(|| anstream::AutoStream::choice(&std::io::stderr()))
519 } else {
520 color_choice
521 }
522}