Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
//! Diagnostic data structures.
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
use std::ops::Range;
/// A severity level for diagnostic messages.
///
/// These are ordered in the following way:
///
/// ```rust
/// use codespan_reporting::diagnostic::Severity;
///
/// assert!(Severity::Bug > Severity::Error);
/// assert!(Severity::Error > Severity::Warning);
/// assert!(Severity::Warning > Severity::Note);
/// assert!(Severity::Note > Severity::Help);
/// ```
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub enum Severity {
/// An unexpected bug.
Bug,
/// An error.
Error,
/// A warning.
Warning,
/// A note.
Note,
/// A help message.
Help,
}
impl Severity {
/// We want bugs to be the maximum severity, errors next, etc...
fn to_cmp_int(self) -> u8 {
match self {
Severity::Bug => 5,
Severity::Error => 4,
Severity::Warning => 3,
Severity::Note => 2,
Severity::Help => 1,
}
}
}
impl PartialOrd for Severity {
fn partial_cmp(&self, other: &Severity) -> Option<std::cmp::Ordering> {
u8::partial_cmp(&self.to_cmp_int(), &other.to_cmp_int())
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub enum LabelStyle {
/// Labels that describe the primary cause of a diagnostic.
Primary,
/// Labels that provide additional context for a diagnostic.
Secondary,
}
/// A label describing an underlined region of code associated with a diagnostic.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct Label<FileId> {
/// The style of the label.
pub style: LabelStyle,
/// The file that we are labelling.
pub file_id: FileId,
/// The range in bytes we are going to include in the final snippet.
pub range: Range<usize>,
/// An optional message to provide some additional information for the
/// underlined code. These should not include line breaks.
pub message: String,
}
impl<FileId> Label<FileId> {
/// Create a new label.
pub fn new(
style: LabelStyle,
file_id: FileId,
range: impl Into<Range<usize>>,
) -> Label<FileId> {
Label {
style,
file_id,
range: range.into(),
message: String::new(),
}
}
/// Create a new label with a style of [`LabelStyle::Primary`].
///
/// [`LabelStyle::Primary`]: LabelStyle::Primary
pub fn primary(file_id: FileId, range: impl Into<Range<usize>>) -> Label<FileId> {
Label::new(LabelStyle::Primary, file_id, range)
}
/// Create a new label with a style of [`LabelStyle::Secondary`].
///
/// [`LabelStyle::Secondary`]: LabelStyle::Secondary
pub fn secondary(file_id: FileId, range: impl Into<Range<usize>>) -> Label<FileId> {
Label::new(LabelStyle::Secondary, file_id, range)
}
/// Add a message to the diagnostic.
pub fn with_message(mut self, message: impl Into<String>) -> Label<FileId> {
self.message = message.into();
self
}
}
/// Represents a diagnostic message that can provide information like errors and
/// warnings to the user.
///
/// The position of a Diagnostic is considered to be the position of the [`Label`] that has the earliest starting position and has the highest style which appears in all the labels of the diagnostic.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct Diagnostic<FileId> {
/// The overall severity of the diagnostic
pub severity: Severity,
/// An optional code that identifies this diagnostic.
pub code: Option<String>,
/// The main message associated with this diagnostic.
///
/// These should not include line breaks, and in order support the 'short'
/// diagnostic display mod, the message should be specific enough to make
/// sense on its own, without additional context provided by labels and notes.
pub message: String,
/// Source labels that describe the cause of the diagnostic.
/// The order of the labels inside the vector does not have any meaning.
/// The labels are always arranged in the order they appear in the source code.
pub labels: Vec<Label<FileId>>,
/// Notes that are associated with the primary cause of the diagnostic.
/// These can include line breaks for improved formatting.
pub notes: Vec<String>,
}
impl<FileId> Diagnostic<FileId> {
/// Create a new diagnostic.
pub fn new(severity: Severity) -> Diagnostic<FileId> {
Diagnostic {
severity,
code: None,
message: String::new(),
labels: Vec::new(),
notes: Vec::new(),
}
}
/// Create a new diagnostic with a severity of [`Severity::Bug`].
///
/// [`Severity::Bug`]: Severity::Bug
pub fn bug() -> Diagnostic<FileId> {
Diagnostic::new(Severity::Bug)
}
/// Create a new diagnostic with a severity of [`Severity::Error`].
///
/// [`Severity::Error`]: Severity::Error
pub fn error() -> Diagnostic<FileId> {
Diagnostic::new(Severity::Error)
}
/// Create a new diagnostic with a severity of [`Severity::Warning`].
///
/// [`Severity::Warning`]: Severity::Warning
pub fn warning() -> Diagnostic<FileId> {
Diagnostic::new(Severity::Warning)
}
/// Create a new diagnostic with a severity of [`Severity::Note`].
///
/// [`Severity::Note`]: Severity::Note
pub fn note() -> Diagnostic<FileId> {
Diagnostic::new(Severity::Note)
}
/// Create a new diagnostic with a severity of [`Severity::Help`].
///
/// [`Severity::Help`]: Severity::Help
pub fn help() -> Diagnostic<FileId> {
Diagnostic::new(Severity::Help)
}
/// Set the error code of the diagnostic.
pub fn with_code(mut self, code: impl Into<String>) -> Diagnostic<FileId> {
self.code = Some(code.into());
self
}
/// Set the message of the diagnostic.
pub fn with_message(mut self, message: impl Into<String>) -> Diagnostic<FileId> {
self.message = message.into();
self
}
/// Add some labels to the diagnostic.
pub fn with_labels(mut self, mut labels: Vec<Label<FileId>>) -> Diagnostic<FileId> {
self.labels.append(&mut labels);
self
}
/// Add some notes to the diagnostic.
pub fn with_notes(mut self, mut notes: Vec<String>) -> Diagnostic<FileId> {
self.notes.append(&mut notes);
self
}
}

443
vendor/codespan-reporting/src/files.rs vendored Normal file
View File

@@ -0,0 +1,443 @@
//! Source file support for diagnostic reporting.
//!
//! The main trait defined in this module is the [`Files`] trait, which provides
//! provides the minimum amount of functionality required for printing [`Diagnostics`]
//! with the [`term::emit`] function.
//!
//! Simple implementations of this trait are implemented:
//!
//! - [`SimpleFile`]: For single-file use-cases
//! - [`SimpleFiles`]: For multi-file use-cases
//!
//! These data structures provide a pretty minimal API, however,
//! so end-users are encouraged to create their own implementations for their
//! own specific use-cases, such as an implementation that accesses the file
//! system directly (and caches the line start locations), or an implementation
//! using an incremental compilation library like [`salsa`].
//!
//! [`term::emit`]: crate::term::emit
//! [`Diagnostics`]: crate::diagnostic::Diagnostic
//! [`Files`]: Files
//! [`SimpleFile`]: SimpleFile
//! [`SimpleFiles`]: SimpleFiles
//!
//! [`salsa`]: https://crates.io/crates/salsa
use std::ops::Range;
/// An enum representing an error that happened while looking up a file or a piece of content in that file.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// A required file is not in the file database.
FileMissing,
/// The file is present, but does not contain the specified byte index.
IndexTooLarge { given: usize, max: usize },
/// The file is present, but does not contain the specified line index.
LineTooLarge { given: usize, max: usize },
/// The file is present and contains the specified line index, but the line does not contain the specified column index.
ColumnTooLarge { given: usize, max: usize },
/// The given index is contained in the file, but is not a boundary of a UTF-8 code point.
InvalidCharBoundary { given: usize },
/// There was a error while doing IO.
Io(std::io::Error),
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::FileMissing => write!(f, "file missing"),
Error::IndexTooLarge { given, max } => {
write!(f, "invalid index {}, maximum index is {}", given, max)
}
Error::LineTooLarge { given, max } => {
write!(f, "invalid line {}, maximum line is {}", given, max)
}
Error::ColumnTooLarge { given, max } => {
write!(f, "invalid column {}, maximum column {}", given, max)
}
Error::InvalidCharBoundary { .. } => write!(f, "index is not a code point boundary"),
Error::Io(err) => write!(f, "{}", err),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
Error::Io(err) => Some(err),
_ => None,
}
}
}
/// A minimal interface for accessing source files when rendering diagnostics.
///
/// A lifetime parameter `'a` is provided to allow any of the returned values to returned by reference.
/// This is to workaround the lack of higher kinded lifetime parameters.
/// This can be ignored if this is not needed, however.
pub trait Files<'a> {
/// A unique identifier for files in the file provider. This will be used
/// for rendering `diagnostic::Label`s in the corresponding source files.
type FileId: 'a + Copy + PartialEq;
/// The user-facing name of a file, to be displayed in diagnostics.
type Name: 'a + std::fmt::Display;
/// The source code of a file.
type Source: 'a + AsRef<str>;
/// The user-facing name of a file.
fn name(&'a self, id: Self::FileId) -> Result<Self::Name, Error>;
/// The source code of a file.
fn source(&'a self, id: Self::FileId) -> Result<Self::Source, Error>;
/// The index of the line at the given byte index.
/// If the byte index is past the end of the file, returns the maximum line index in the file.
/// This means that this function only fails if the file is not present.
///
/// # Note for trait implementors
///
/// This can be implemented efficiently by performing a binary search over
/// a list of line starts that was computed by calling the [`line_starts`]
/// function that is exported from the [`files`] module. It might be useful
/// to pre-compute and cache these line starts.
///
/// [`line_starts`]: crate::files::line_starts
/// [`files`]: crate::files
fn line_index(&'a self, id: Self::FileId, byte_index: usize) -> Result<usize, Error>;
/// The user-facing line number at the given line index.
/// It is not necessarily checked that the specified line index
/// is actually in the file.
///
/// # Note for trait implementors
///
/// This is usually 1-indexed from the beginning of the file, but
/// can be useful for implementing something like the
/// [C preprocessor's `#line` macro][line-macro].
///
/// [line-macro]: https://en.cppreference.com/w/c/preprocessor/line
#[allow(unused_variables)]
fn line_number(&'a self, id: Self::FileId, line_index: usize) -> Result<usize, Error> {
Ok(line_index + 1)
}
/// The user-facing column number at the given line index and byte index.
///
/// # Note for trait implementors
///
/// This is usually 1-indexed from the the start of the line.
/// A default implementation is provided, based on the [`column_index`]
/// function that is exported from the [`files`] module.
///
/// [`files`]: crate::files
/// [`column_index`]: crate::files::column_index
fn column_number(
&'a self,
id: Self::FileId,
line_index: usize,
byte_index: usize,
) -> Result<usize, Error> {
let source = self.source(id)?;
let line_range = self.line_range(id, line_index)?;
let column_index = column_index(source.as_ref(), line_range, byte_index);
Ok(column_index + 1)
}
/// Convenience method for returning line and column number at the given
/// byte index in the file.
fn location(&'a self, id: Self::FileId, byte_index: usize) -> Result<Location, Error> {
let line_index = self.line_index(id, byte_index)?;
Ok(Location {
line_number: self.line_number(id, line_index)?,
column_number: self.column_number(id, line_index, byte_index)?,
})
}
/// The byte range of line in the source of the file.
fn line_range(&'a self, id: Self::FileId, line_index: usize) -> Result<Range<usize>, Error>;
}
/// A user-facing location in a source file.
///
/// Returned by [`Files::location`].
///
/// [`Files::location`]: Files::location
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Location {
/// The user-facing line number.
pub line_number: usize,
/// The user-facing column number.
pub column_number: usize,
}
/// The column index at the given byte index in the source file.
/// This is the number of characters to the given byte index.
///
/// If the byte index is smaller than the start of the line, then `0` is returned.
/// If the byte index is past the end of the line, the column index of the last
/// character `+ 1` is returned.
///
/// # Example
///
/// ```rust
/// use codespan_reporting::files;
///
/// let source = "\n\n🗻∈🌏\n\n";
///
/// assert_eq!(files::column_index(source, 0..1, 0), 0);
/// assert_eq!(files::column_index(source, 2..13, 0), 0);
/// assert_eq!(files::column_index(source, 2..13, 2 + 0), 0);
/// assert_eq!(files::column_index(source, 2..13, 2 + 1), 0);
/// assert_eq!(files::column_index(source, 2..13, 2 + 4), 1);
/// assert_eq!(files::column_index(source, 2..13, 2 + 8), 2);
/// assert_eq!(files::column_index(source, 2..13, 2 + 10), 2);
/// assert_eq!(files::column_index(source, 2..13, 2 + 11), 3);
/// assert_eq!(files::column_index(source, 2..13, 2 + 12), 3);
/// ```
pub fn column_index(source: &str, line_range: Range<usize>, byte_index: usize) -> usize {
let end_index = std::cmp::min(byte_index, std::cmp::min(line_range.end, source.len()));
(line_range.start..end_index)
.filter(|byte_index| source.is_char_boundary(byte_index + 1))
.count()
}
/// Return the starting byte index of each line in the source string.
///
/// This can make it easier to implement [`Files::line_index`] by allowing
/// implementors of [`Files`] to pre-compute the line starts, then search for
/// the corresponding line range, as shown in the example below.
///
/// [`Files`]: Files
/// [`Files::line_index`]: Files::line_index
///
/// # Example
///
/// ```rust
/// use codespan_reporting::files;
///
/// let source = "foo\nbar\r\n\nbaz";
/// let line_starts: Vec<_> = files::line_starts(source).collect();
///
/// assert_eq!(
/// line_starts,
/// [
/// 0, // "foo\n"
/// 4, // "bar\r\n"
/// 9, // ""
/// 10, // "baz"
/// ],
/// );
///
/// fn line_index(line_starts: &[usize], byte_index: usize) -> Option<usize> {
/// match line_starts.binary_search(&byte_index) {
/// Ok(line) => Some(line),
/// Err(next_line) => Some(next_line - 1),
/// }
/// }
///
/// assert_eq!(line_index(&line_starts, 5), Some(1));
/// ```
// NOTE: this is copied in `codespan::file::line_starts` and should be kept in sync.
pub fn line_starts<'source>(source: &'source str) -> impl 'source + Iterator<Item = usize> {
std::iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1))
}
/// A file database that contains a single source file.
///
/// Because there is only single file in this database we use `()` as a [`FileId`].
///
/// This is useful for simple language tests, but it might be worth creating a
/// custom implementation when a language scales beyond a certain size.
///
/// [`FileId`]: Files::FileId
#[derive(Debug, Clone)]
pub struct SimpleFile<Name, Source> {
/// The name of the file.
name: Name,
/// The source code of the file.
source: Source,
/// The starting byte indices in the source code.
line_starts: Vec<usize>,
}
impl<Name, Source> SimpleFile<Name, Source>
where
Name: std::fmt::Display,
Source: AsRef<str>,
{
/// Create a new source file.
pub fn new(name: Name, source: Source) -> SimpleFile<Name, Source> {
SimpleFile {
name,
line_starts: line_starts(source.as_ref()).collect(),
source,
}
}
/// Return the name of the file.
pub fn name(&self) -> &Name {
&self.name
}
/// Return the source of the file.
pub fn source(&self) -> &Source {
&self.source
}
/// Return the starting byte index of the line with the specified line index.
/// Convenience method that already generates errors if necessary.
fn line_start(&self, line_index: usize) -> Result<usize, Error> {
use std::cmp::Ordering;
match line_index.cmp(&self.line_starts.len()) {
Ordering::Less => Ok(self
.line_starts
.get(line_index)
.cloned()
.expect("failed despite previous check")),
Ordering::Equal => Ok(self.source.as_ref().len()),
Ordering::Greater => Err(Error::LineTooLarge {
given: line_index,
max: self.line_starts.len() - 1,
}),
}
}
}
impl<'a, Name, Source> Files<'a> for SimpleFile<Name, Source>
where
Name: 'a + std::fmt::Display + Clone,
Source: 'a + AsRef<str>,
{
type FileId = ();
type Name = Name;
type Source = &'a str;
fn name(&self, (): ()) -> Result<Name, Error> {
Ok(self.name.clone())
}
fn source(&self, (): ()) -> Result<&str, Error> {
Ok(self.source.as_ref())
}
fn line_index(&self, (): (), byte_index: usize) -> Result<usize, Error> {
Ok(self
.line_starts
.binary_search(&byte_index)
.unwrap_or_else(|next_line| next_line - 1))
}
fn line_range(&self, (): (), line_index: usize) -> Result<Range<usize>, Error> {
let line_start = self.line_start(line_index)?;
let next_line_start = self.line_start(line_index + 1)?;
Ok(line_start..next_line_start)
}
}
/// A file database that can store multiple source files.
///
/// This is useful for simple language tests, but it might be worth creating a
/// custom implementation when a language scales beyond a certain size.
/// It is a glorified `Vec<SimpleFile>` that implements the `Files` trait.
#[derive(Debug, Clone)]
pub struct SimpleFiles<Name, Source> {
files: Vec<SimpleFile<Name, Source>>,
}
impl<Name, Source> SimpleFiles<Name, Source>
where
Name: std::fmt::Display,
Source: AsRef<str>,
{
/// Create a new files database.
pub fn new() -> SimpleFiles<Name, Source> {
SimpleFiles { files: Vec::new() }
}
/// Add a file to the database, returning the handle that can be used to
/// refer to it again.
pub fn add(&mut self, name: Name, source: Source) -> usize {
let file_id = self.files.len();
self.files.push(SimpleFile::new(name, source));
file_id
}
/// Get the file corresponding to the given id.
pub fn get(&self, file_id: usize) -> Result<&SimpleFile<Name, Source>, Error> {
self.files.get(file_id).ok_or(Error::FileMissing)
}
}
impl<'a, Name, Source> Files<'a> for SimpleFiles<Name, Source>
where
Name: 'a + std::fmt::Display + Clone,
Source: 'a + AsRef<str>,
{
type FileId = usize;
type Name = Name;
type Source = &'a str;
fn name(&self, file_id: usize) -> Result<Name, Error> {
Ok(self.get(file_id)?.name().clone())
}
fn source(&self, file_id: usize) -> Result<&str, Error> {
Ok(self.get(file_id)?.source().as_ref())
}
fn line_index(&self, file_id: usize, byte_index: usize) -> Result<usize, Error> {
self.get(file_id)?.line_index((), byte_index)
}
fn line_range(&self, file_id: usize, line_index: usize) -> Result<Range<usize>, Error> {
self.get(file_id)?.line_range((), line_index)
}
}
#[cfg(test)]
mod test {
use super::*;
const TEST_SOURCE: &str = "foo\nbar\r\n\nbaz";
#[test]
fn line_starts() {
let file = SimpleFile::new("test", TEST_SOURCE);
assert_eq!(
file.line_starts,
[
0, // "foo\n"
4, // "bar\r\n"
9, // ""
10, // "baz"
],
);
}
#[test]
fn line_span_sources() {
let file = SimpleFile::new("test", TEST_SOURCE);
let line_sources = (0..4)
.map(|line| {
let line_range = file.line_range((), line).unwrap();
&file.source[line_range]
})
.collect::<Vec<_>>();
assert_eq!(line_sources, ["foo\n", "bar\r\n", "\n", "baz"]);
}
}

7
vendor/codespan-reporting/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,7 @@
//! Diagnostic reporting support for the codespan crate.
#![forbid(unsafe_code)]
pub mod diagnostic;
pub mod files;
pub mod term;

121
vendor/codespan-reporting/src/term.rs vendored Normal file
View File

@@ -0,0 +1,121 @@
//! Terminal back-end for emitting diagnostics.
use std::str::FromStr;
use termcolor::{ColorChoice, WriteColor};
use crate::diagnostic::Diagnostic;
use crate::files::Files;
mod config;
mod renderer;
mod views;
pub use termcolor;
pub use self::config::{Chars, Config, DisplayStyle, Styles};
/// A command line argument that configures the coloring of the output.
///
/// This can be used with command line argument parsers like [`clap`] or [`structopt`].
///
/// [`clap`]: https://crates.io/crates/clap
/// [`structopt`]: https://crates.io/crates/structopt
///
/// # Example
///
/// ```rust
/// use codespan_reporting::term::termcolor::StandardStream;
/// use codespan_reporting::term::ColorArg;
/// use structopt::StructOpt;
///
/// #[derive(Debug, StructOpt)]
/// #[structopt(name = "groovey-app")]
/// pub struct Opts {
/// /// Configure coloring of output
/// #[structopt(
/// long = "color",
/// default_value = "auto",
/// possible_values = ColorArg::VARIANTS,
/// case_insensitive = true,
/// )]
/// pub color: ColorArg,
/// }
///
/// let opts = Opts::from_args();
/// let writer = StandardStream::stderr(opts.color.into());
/// ```
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ColorArg(pub ColorChoice);
impl ColorArg {
/// Allowed values the argument.
///
/// This is useful for generating documentation via [`clap`] or `structopt`'s
/// `possible_values` configuration.
///
/// [`clap`]: https://crates.io/crates/clap
/// [`structopt`]: https://crates.io/crates/structopt
pub const VARIANTS: &'static [&'static str] = &["auto", "always", "ansi", "never"];
}
impl FromStr for ColorArg {
type Err = &'static str;
fn from_str(src: &str) -> Result<ColorArg, &'static str> {
match src {
_ if src.eq_ignore_ascii_case("auto") => Ok(ColorArg(ColorChoice::Auto)),
_ if src.eq_ignore_ascii_case("always") => Ok(ColorArg(ColorChoice::Always)),
_ if src.eq_ignore_ascii_case("ansi") => Ok(ColorArg(ColorChoice::AlwaysAnsi)),
_ if src.eq_ignore_ascii_case("never") => Ok(ColorArg(ColorChoice::Never)),
_ => Err("valid values: auto, always, ansi, never"),
}
}
}
impl Into<ColorChoice> for ColorArg {
fn into(self) -> ColorChoice {
self.0
}
}
/// Emit a diagnostic using the given writer, context, config, and files.
///
/// The return value covers all error cases. These error case can arise if:
/// * a file was removed from the file database.
/// * a file was changed so that it is too small to have an index
/// * IO fails
pub fn emit<'files, F: Files<'files>>(
writer: &mut dyn WriteColor,
config: &Config,
files: &'files F,
diagnostic: &Diagnostic<F::FileId>,
) -> Result<(), super::files::Error> {
use self::renderer::Renderer;
use self::views::{RichDiagnostic, ShortDiagnostic};
let mut renderer = Renderer::new(writer, config);
match config.display_style {
DisplayStyle::Rich => RichDiagnostic::new(diagnostic, config).render(files, &mut renderer),
DisplayStyle::Medium => ShortDiagnostic::new(diagnostic, true).render(files, &mut renderer),
DisplayStyle::Short => ShortDiagnostic::new(diagnostic, false).render(files, &mut renderer),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostic::Label;
use crate::files::SimpleFiles;
#[test]
fn unsized_emit() {
let mut files = SimpleFiles::new();
let id = files.add("test", "");
let mut writer = termcolor::NoColor::new(Vec::<u8>::new());
let diagnostic = Diagnostic::bug().with_labels(vec![Label::primary(id, 0..0)]);
emit(&mut writer, &Config::default(), &files, &diagnostic).unwrap();
}
}

View File

@@ -0,0 +1,321 @@
use termcolor::{Color, ColorSpec};
use crate::diagnostic::{LabelStyle, Severity};
/// Configures how a diagnostic is rendered.
#[derive(Clone, Debug)]
pub struct Config {
/// The display style to use when rendering diagnostics.
/// Defaults to: [`DisplayStyle::Rich`].
///
/// [`DisplayStyle::Rich`]: DisplayStyle::Rich
pub display_style: DisplayStyle,
/// Column width of tabs.
/// Defaults to: `4`.
pub tab_width: usize,
/// Styles to use when rendering the diagnostic.
pub styles: Styles,
/// Characters to use when rendering the diagnostic.
pub chars: Chars,
/// The minimum number of lines to be shown after the line on which a multiline [`Label`] begins.
///
/// Defaults to: `3`.
///
/// [`Label`]: crate::diagnostic::Label
pub start_context_lines: usize,
/// The minimum number of lines to be shown before the line on which a multiline [`Label`] ends.
///
/// Defaults to: `1`.
///
/// [`Label`]: crate::diagnostic::Label
pub end_context_lines: usize,
}
impl Default for Config {
fn default() -> Config {
Config {
display_style: DisplayStyle::Rich,
tab_width: 4,
styles: Styles::default(),
chars: Chars::default(),
start_context_lines: 3,
end_context_lines: 1,
}
}
}
/// The display style to use when rendering diagnostics.
#[derive(Clone, Debug)]
pub enum DisplayStyle {
/// Output a richly formatted diagnostic, with source code previews.
///
/// ```text
/// error[E0001]: unexpected type in `+` application
/// ┌─ test:2:9
/// │
/// 2 │ (+ test "")
/// │ ^^ expected `Int` but found `String`
/// │
/// = expected type `Int`
/// found type `String`
///
/// error[E0002]: Bad config found
///
/// ```
Rich,
/// Output a condensed diagnostic, with a line number, severity, message and notes (if any).
///
/// ```text
/// test:2:9: error[E0001]: unexpected type in `+` application
/// = expected type `Int`
/// found type `String`
///
/// error[E0002]: Bad config found
/// ```
Medium,
/// Output a short diagnostic, with a line number, severity, and message.
///
/// ```text
/// test:2:9: error[E0001]: unexpected type in `+` application
/// error[E0002]: Bad config found
/// ```
Short,
}
/// Styles to use when rendering the diagnostic.
#[derive(Clone, Debug)]
pub struct Styles {
/// The style to use when rendering bug headers.
/// Defaults to `fg:red bold intense`.
pub header_bug: ColorSpec,
/// The style to use when rendering error headers.
/// Defaults to `fg:red bold intense`.
pub header_error: ColorSpec,
/// The style to use when rendering warning headers.
/// Defaults to `fg:yellow bold intense`.
pub header_warning: ColorSpec,
/// The style to use when rendering note headers.
/// Defaults to `fg:green bold intense`.
pub header_note: ColorSpec,
/// The style to use when rendering help headers.
/// Defaults to `fg:cyan bold intense`.
pub header_help: ColorSpec,
/// The style to use when the main diagnostic message.
/// Defaults to `bold intense`.
pub header_message: ColorSpec,
/// The style to use when rendering bug labels.
/// Defaults to `fg:red`.
pub primary_label_bug: ColorSpec,
/// The style to use when rendering error labels.
/// Defaults to `fg:red`.
pub primary_label_error: ColorSpec,
/// The style to use when rendering warning labels.
/// Defaults to `fg:yellow`.
pub primary_label_warning: ColorSpec,
/// The style to use when rendering note labels.
/// Defaults to `fg:green`.
pub primary_label_note: ColorSpec,
/// The style to use when rendering help labels.
/// Defaults to `fg:cyan`.
pub primary_label_help: ColorSpec,
/// The style to use when rendering secondary labels.
/// Defaults `fg:blue` (or `fg:cyan` on windows).
pub secondary_label: ColorSpec,
/// The style to use when rendering the line numbers.
/// Defaults `fg:blue` (or `fg:cyan` on windows).
pub line_number: ColorSpec,
/// The style to use when rendering the source code borders.
/// Defaults `fg:blue` (or `fg:cyan` on windows).
pub source_border: ColorSpec,
/// The style to use when rendering the note bullets.
/// Defaults `fg:blue` (or `fg:cyan` on windows).
pub note_bullet: ColorSpec,
}
impl Styles {
/// The style used to mark a header at a given severity.
pub fn header(&self, severity: Severity) -> &ColorSpec {
match severity {
Severity::Bug => &self.header_bug,
Severity::Error => &self.header_error,
Severity::Warning => &self.header_warning,
Severity::Note => &self.header_note,
Severity::Help => &self.header_help,
}
}
/// The style used to mark a primary or secondary label at a given severity.
pub fn label(&self, severity: Severity, label_style: LabelStyle) -> &ColorSpec {
match (label_style, severity) {
(LabelStyle::Primary, Severity::Bug) => &self.primary_label_bug,
(LabelStyle::Primary, Severity::Error) => &self.primary_label_error,
(LabelStyle::Primary, Severity::Warning) => &self.primary_label_warning,
(LabelStyle::Primary, Severity::Note) => &self.primary_label_note,
(LabelStyle::Primary, Severity::Help) => &self.primary_label_help,
(LabelStyle::Secondary, _) => &self.secondary_label,
}
}
#[doc(hidden)]
pub fn with_blue(blue: Color) -> Styles {
let header = ColorSpec::new().set_bold(true).set_intense(true).clone();
Styles {
header_bug: header.clone().set_fg(Some(Color::Red)).clone(),
header_error: header.clone().set_fg(Some(Color::Red)).clone(),
header_warning: header.clone().set_fg(Some(Color::Yellow)).clone(),
header_note: header.clone().set_fg(Some(Color::Green)).clone(),
header_help: header.clone().set_fg(Some(Color::Cyan)).clone(),
header_message: header,
primary_label_bug: ColorSpec::new().set_fg(Some(Color::Red)).clone(),
primary_label_error: ColorSpec::new().set_fg(Some(Color::Red)).clone(),
primary_label_warning: ColorSpec::new().set_fg(Some(Color::Yellow)).clone(),
primary_label_note: ColorSpec::new().set_fg(Some(Color::Green)).clone(),
primary_label_help: ColorSpec::new().set_fg(Some(Color::Cyan)).clone(),
secondary_label: ColorSpec::new().set_fg(Some(blue)).clone(),
line_number: ColorSpec::new().set_fg(Some(blue)).clone(),
source_border: ColorSpec::new().set_fg(Some(blue)).clone(),
note_bullet: ColorSpec::new().set_fg(Some(blue)).clone(),
}
}
}
impl Default for Styles {
fn default() -> Styles {
// Blue is really difficult to see on the standard windows command line
#[cfg(windows)]
const BLUE: Color = Color::Cyan;
#[cfg(not(windows))]
const BLUE: Color = Color::Blue;
Self::with_blue(BLUE)
}
}
/// Characters to use when rendering the diagnostic.
///
/// By using [`Chars::ascii()`] you can switch to an ASCII-only format suitable
/// for rendering on terminals that do not support box drawing characters.
#[derive(Clone, Debug)]
pub struct Chars {
/// The characters to use for the top-left border of the snippet.
/// Defaults to: `"┌─"` or `"-->"` with [`Chars::ascii()`].
pub snippet_start: String,
/// The character to use for the left border of the source.
/// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`].
pub source_border_left: char,
/// The character to use for the left border break of the source.
/// Defaults to: `'·'` or `'.'` with [`Chars::ascii()`].
pub source_border_left_break: char,
/// The character to use for the note bullet.
/// Defaults to: `'='`.
pub note_bullet: char,
/// The character to use for marking a single-line primary label.
/// Defaults to: `'^'`.
pub single_primary_caret: char,
/// The character to use for marking a single-line secondary label.
/// Defaults to: `'-'`.
pub single_secondary_caret: char,
/// The character to use for marking the start of a multi-line primary label.
/// Defaults to: `'^'`.
pub multi_primary_caret_start: char,
/// The character to use for marking the end of a multi-line primary label.
/// Defaults to: `'^'`.
pub multi_primary_caret_end: char,
/// The character to use for marking the start of a multi-line secondary label.
/// Defaults to: `'\''`.
pub multi_secondary_caret_start: char,
/// The character to use for marking the end of a multi-line secondary label.
/// Defaults to: `'\''`.
pub multi_secondary_caret_end: char,
/// The character to use for the top-left corner of a multi-line label.
/// Defaults to: `'╭'` or `'/'` with [`Chars::ascii()`].
pub multi_top_left: char,
/// The character to use for the top of a multi-line label.
/// Defaults to: `'─'` or `'-'` with [`Chars::ascii()`].
pub multi_top: char,
/// The character to use for the bottom-left corner of a multi-line label.
/// Defaults to: `'╰'` or `'\'` with [`Chars::ascii()`].
pub multi_bottom_left: char,
/// The character to use when marking the bottom of a multi-line label.
/// Defaults to: `'─'` or `'-'` with [`Chars::ascii()`].
pub multi_bottom: char,
/// The character to use for the left of a multi-line label.
/// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`].
pub multi_left: char,
/// The character to use for the left of a pointer underneath a caret.
/// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`].
pub pointer_left: char,
}
impl Default for Chars {
fn default() -> Chars {
Chars::box_drawing()
}
}
impl Chars {
/// A character set that uses Unicode box drawing characters.
pub fn box_drawing() -> Chars {
Chars {
snippet_start: "┌─".into(),
source_border_left: '',
source_border_left_break: '·',
note_bullet: '=',
single_primary_caret: '^',
single_secondary_caret: '-',
multi_primary_caret_start: '^',
multi_primary_caret_end: '^',
multi_secondary_caret_start: '\'',
multi_secondary_caret_end: '\'',
multi_top_left: '',
multi_top: '',
multi_bottom_left: '',
multi_bottom: '',
multi_left: '',
pointer_left: '',
}
}
/// A character set that only uses ASCII characters.
///
/// This is useful if your terminal's font does not support box drawing
/// characters well and results in output that looks similar to rustc's
/// diagnostic output.
pub fn ascii() -> Chars {
Chars {
snippet_start: "-->".into(),
source_border_left: '|',
source_border_left_break: '.',
note_bullet: '=',
single_primary_caret: '^',
single_secondary_caret: '-',
multi_primary_caret_start: '^',
multi_primary_caret_end: '^',
multi_secondary_caret_start: '\'',
multi_secondary_caret_end: '\'',
multi_top_left: '/',
multi_top: '-',
multi_bottom_left: '\\',
multi_bottom: '-',
multi_left: '|',
pointer_left: '|',
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,478 @@
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(())
}
}