Revision control

Copy as Markdown

Other Tools

use std::ops::Range;
use crate::diagnostic::{Diagnostic, LabelStyle};
use crate::files::{Error, Files, Location};
use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel};
use crate::term::Config;
/// Count the number of decimal digits in `n`.
fn count_digits(mut n: usize) -> usize {
let mut count = 0;
while n != 0 {
count += 1;
n /= 10; // remove last digit
}
count
}
/// Output a richly formatted diagnostic, with source code previews.
pub struct RichDiagnostic<'diagnostic, 'config, FileId> {
diagnostic: &'diagnostic Diagnostic<FileId>,
config: &'config Config,
}
impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId>
where
FileId: Copy + PartialEq,
{
pub fn new(
diagnostic: &'diagnostic Diagnostic<FileId>,
config: &'config Config,
) -> RichDiagnostic<'diagnostic, 'config, FileId> {
RichDiagnostic { diagnostic, config }
}
pub fn render<'files>(
&self,
files: &'files impl Files<'files, FileId = FileId>,
renderer: &mut Renderer<'_, '_>,
) -> Result<(), Error>
where
FileId: 'files,
{
use std::collections::BTreeMap;
struct LabeledFile<'diagnostic, FileId> {
file_id: FileId,
start: usize,
name: String,
location: Location,
num_multi_labels: usize,
lines: BTreeMap<usize, Line<'diagnostic>>,
max_label_style: LabelStyle,
}
impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> {
fn get_or_insert_line(
&mut self,
line_index: usize,
line_range: Range<usize>,
line_number: usize,
) -> &mut Line<'diagnostic> {
self.lines.entry(line_index).or_insert_with(|| Line {
range: line_range,
number: line_number,
single_labels: vec![],
multi_labels: vec![],
// This has to be false by default so we know if it must be rendered by another condition already.
must_render: false,
})
}
}
struct Line<'diagnostic> {
number: usize,
range: std::ops::Range<usize>,
// TODO: How do we reuse these allocations?
single_labels: Vec<SingleLabel<'diagnostic>>,
multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>,
must_render: bool,
}
// TODO: Make this data structure external, to allow for allocation reuse
let mut labeled_files = Vec::<LabeledFile<'_, _>>::new();
// Keep track of the outer padding to use when rendering the
// snippets of source code.
let mut outer_padding = 0;
// Group labels by file
for label in &self.diagnostic.labels {
let start_line_index = files.line_index(label.file_id, label.range.start)?;
let start_line_number = files.line_number(label.file_id, start_line_index)?;
let start_line_range = files.line_range(label.file_id, start_line_index)?;
let end_line_index = files.line_index(label.file_id, label.range.end)?;
let end_line_number = files.line_number(label.file_id, end_line_index)?;
let end_line_range = files.line_range(label.file_id, end_line_index)?;
outer_padding = std::cmp::max(outer_padding, count_digits(start_line_number));
outer_padding = std::cmp::max(outer_padding, count_digits(end_line_number));
// NOTE: This could be made more efficient by using an associative
// data structure like a hashmap or B-tree, but we use a vector to
// preserve the order that unique files appear in the list of labels.
let labeled_file = match labeled_files
.iter_mut()
.find(|labeled_file| label.file_id == labeled_file.file_id)
{
Some(labeled_file) => {
// another diagnostic also referenced this file
if labeled_file.max_label_style > label.style
|| (labeled_file.max_label_style == label.style
&& labeled_file.start > label.range.start)
{
// this label has a higher style or has the same style but starts earlier
labeled_file.start = label.range.start;
labeled_file.location = files.location(label.file_id, label.range.start)?;
labeled_file.max_label_style = label.style;
}
labeled_file
}
None => {
// no other diagnostic referenced this file yet
labeled_files.push(LabeledFile {
file_id: label.file_id,
start: label.range.start,
name: files.name(label.file_id)?.to_string(),
location: files.location(label.file_id, label.range.start)?,
num_multi_labels: 0,
lines: BTreeMap::new(),
max_label_style: label.style,
});
// this unwrap should never fail because we just pushed an element
labeled_files
.last_mut()
.expect("just pushed an element that disappeared")
}
};
if start_line_index == end_line_index {
// Single line
//
// ```text
// 2 โ”‚ (+ test "")
// โ”‚ ^^ expected `Int` but found `String`
// ```
let label_start = label.range.start - start_line_range.start;
// Ensure that we print at least one caret, even when we
// have a zero-length source range.
let label_end =
usize::max(label.range.end - start_line_range.start, label_start + 1);
let line = labeled_file.get_or_insert_line(
start_line_index,
start_line_range,
start_line_number,
);
// Ensure that the single line labels are lexicographically
// sorted by the range of source code that they cover.
let index = match line.single_labels.binary_search_by(|(_, range, _)| {
// `Range<usize>` doesn't implement `Ord`, so convert to `(usize, usize)`
// to piggyback off its lexicographic comparison implementation.
(range.start, range.end).cmp(&(label_start, label_end))
}) {
// If the ranges are the same, order the labels in reverse
// to how they were originally specified in the diagnostic.
// This helps with printing in the renderer.
Ok(index) | Err(index) => index,
};
line.single_labels
.insert(index, (label.style, label_start..label_end, &label.message));
// If this line is not rendered, the SingleLabel is not visible.
line.must_render = true;
} else {
// Multiple lines
//
// ```text
// 4 โ”‚ fizzโ‚ num = case (mod num 5) (mod num 3) of
// โ”‚ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€^
// 5 โ”‚ โ”‚ 0 0 => "FizzBuzz"
// 6 โ”‚ โ”‚ 0 _ => "Fizz"
// 7 โ”‚ โ”‚ _ 0 => "Buzz"
// 8 โ”‚ โ”‚ _ _ => num
// โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€^ `case` clauses have incompatible types
// ```
let label_index = labeled_file.num_multi_labels;
labeled_file.num_multi_labels += 1;
// First labeled line
let label_start = label.range.start - start_line_range.start;
let start_line = labeled_file.get_or_insert_line(
start_line_index,
start_line_range.clone(),
start_line_number,
);
start_line.multi_labels.push((
label_index,
label.style,
MultiLabel::Top(label_start),
));
// The first line has to be rendered so the start of the label is visible.
start_line.must_render = true;
// Marked lines
//
// ```text
// 5 โ”‚ โ”‚ 0 0 => "FizzBuzz"
// 6 โ”‚ โ”‚ 0 _ => "Fizz"
// 7 โ”‚ โ”‚ _ 0 => "Buzz"
// ```
for line_index in (start_line_index + 1)..end_line_index {
let line_range = files.line_range(label.file_id, line_index)?;
let line_number = files.line_number(label.file_id, line_index)?;
outer_padding = std::cmp::max(outer_padding, count_digits(line_number));
let line = labeled_file.get_or_insert_line(line_index, line_range, line_number);
line.multi_labels
.push((label_index, label.style, MultiLabel::Left));
// The line should be rendered to match the configuration of how much context to show.
line.must_render |=
// Is this line part of the context after the start of the label?
line_index - start_line_index <= self.config.start_context_lines
||
// Is this line part of the context before the end of the label?
end_line_index - line_index <= self.config.end_context_lines;
}
// Last labeled line
//
// ```text
// 8 โ”‚ โ”‚ _ _ => num
// โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€^ `case` clauses have incompatible types
// ```
let label_end = label.range.end - end_line_range.start;
let end_line = labeled_file.get_or_insert_line(
end_line_index,
end_line_range,
end_line_number,
);
end_line.multi_labels.push((
label_index,
label.style,
MultiLabel::Bottom(label_end, &label.message),
));
// The last line has to be rendered so the end of the label is visible.
end_line.must_render = true;
}
}
// Header and message
//
// ```text
// error[E0001]: unexpected type in `+` application
// ```
renderer.render_header(
None,
self.diagnostic.severity,
self.diagnostic.code.as_deref(),
self.diagnostic.message.as_str(),
)?;
// Source snippets
//
// ```text
// โ”Œโ”€ test:2:9
// โ”‚
// 2 โ”‚ (+ test "")
// โ”‚ ^^ expected `Int` but found `String`
// โ”‚
// ```
let mut labeled_files = labeled_files.into_iter().peekable();
while let Some(labeled_file) = labeled_files.next() {
let source = files.source(labeled_file.file_id)?;
let source = source.as_ref();
// Top left border and locus.
//
// ```text
// โ”Œโ”€ test:2:9
// ```
if !labeled_file.lines.is_empty() {
renderer.render_snippet_start(
outer_padding,
&Locus {
name: labeled_file.name,
location: labeled_file.location,
},
)?;
renderer.render_snippet_empty(
outer_padding,
self.diagnostic.severity,
labeled_file.num_multi_labels,
&[],
)?;
}
let mut lines = labeled_file
.lines
.iter()
.filter(|(_, line)| line.must_render)
.peekable();
while let Some((line_index, line)) = lines.next() {
renderer.render_snippet_source(
outer_padding,
line.number,
&source[line.range.clone()],
self.diagnostic.severity,
&line.single_labels,
labeled_file.num_multi_labels,
&line.multi_labels,
)?;
// Check to see if we need to render any intermediate stuff
// before rendering the next line.
if let Some((next_line_index, _)) = lines.peek() {
match next_line_index.checked_sub(*line_index) {
// Consecutive lines
Some(1) => {}
// One line between the current line and the next line
Some(2) => {
// Write a source line
let file_id = labeled_file.file_id;
// This line was not intended to be rendered initially.
// To render the line right, we have to get back the original labels.
let labels = labeled_file
.lines
.get(&(line_index + 1))
.map_or(&[][..], |line| &line.multi_labels[..]);
renderer.render_snippet_source(
outer_padding,
files.line_number(file_id, line_index + 1)?,
&source[files.line_range(file_id, line_index + 1)?],
self.diagnostic.severity,
&[],
labeled_file.num_multi_labels,
labels,
)?;
}
// More than one line between the current line and the next line.
Some(_) | None => {
// Source break
//
// ```text
// ยท
// ```
renderer.render_snippet_break(
outer_padding,
self.diagnostic.severity,
labeled_file.num_multi_labels,
&line.multi_labels,
)?;
}
}
}
}
// Check to see if we should render a trailing border after the
// final line of the snippet.
if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() {
// We don't render a border if we are at the final newline
// without trailing notes, because it would end up looking too
// spaced-out in combination with the final new line.
} else {
// Render the trailing snippet border.
renderer.render_snippet_empty(
outer_padding,
self.diagnostic.severity,
labeled_file.num_multi_labels,
&[],
)?;
}
}
// Additional notes
//
// ```text
// = expected type `Int`
// found type `String`
// ```
for note in &self.diagnostic.notes {
renderer.render_snippet_note(outer_padding, note)?;
}
renderer.render_empty()
}
}
/// Output a short diagnostic, with a line number, severity, and message.
pub struct ShortDiagnostic<'diagnostic, FileId> {
diagnostic: &'diagnostic Diagnostic<FileId>,
show_notes: bool,
}
impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId>
where
FileId: Copy + PartialEq,
{
pub fn new(
diagnostic: &'diagnostic Diagnostic<FileId>,
show_notes: bool,
) -> ShortDiagnostic<'diagnostic, FileId> {
ShortDiagnostic {
diagnostic,
show_notes,
}
}
pub fn render<'files>(
&self,
files: &'files impl Files<'files, FileId = FileId>,
renderer: &mut Renderer<'_, '_>,
) -> Result<(), Error>
where
FileId: 'files,
{
// Located headers
//
// ```text
// test:2:9: error[E0001]: unexpected type in `+` application
// ```
let mut primary_labels_encountered = 0;
let labels = self.diagnostic.labels.iter();
for label in labels.filter(|label| label.style == LabelStyle::Primary) {
primary_labels_encountered += 1;
renderer.render_header(
Some(&Locus {
name: files.name(label.file_id)?.to_string(),
location: files.location(label.file_id, label.range.start)?,
}),
self.diagnostic.severity,
self.diagnostic.code.as_deref(),
self.diagnostic.message.as_str(),
)?;
}
// Fallback to printing a non-located header if no primary labels were encountered
//
// ```text
// error[E0002]: Bad config found
// ```
if primary_labels_encountered == 0 {
renderer.render_header(
None,
self.diagnostic.severity,
self.diagnostic.code.as_deref(),
self.diagnostic.message.as_str(),
)?;
}
if self.show_notes {
// Additional notes
//
// ```text
// = expected type `Int`
// found type `String`
// ```
for note in &self.diagnostic.notes {
renderer.render_snippet_note(0, note)?;
}
}
Ok(())
}
}