Merge pull request #66 from brendanzab/components

Make diagnostics look more like what the LSP expects
This commit is contained in:
Brendan Zabarauskas
2019-05-30 22:44:34 +10:00
committed by GitHub
5 changed files with 378 additions and 325 deletions
+11 -28
View File
@@ -180,8 +180,6 @@ pub fn make_lsp_diagnostic<F>(
where
F: FnMut(&str) -> Result<Url, ()>,
{
use codespan_reporting::LabelStyle;
let find_file = |index| {
code_map
.find_file(index)
@@ -189,37 +187,22 @@ where
};
// We need a position for the primary error so take the span from the first primary label
let (primary_file_map, primary_label_range) = {
let first_primary_label = diagnostic
.labels
.iter()
.find(|label| label.style == LabelStyle::Primary);
match first_primary_label {
Some(label) => {
let file_map = find_file(label.span.start())?;
(Some(file_map), byte_span_to_range(&file_map, label.span)?)
},
None => (None, UNKNOWN_RANGE),
}
};
let primary_file_map = find_file(diagnostic.primary_label.span.start())?;
let primary_label_range = byte_span_to_range(&primary_file_map, diagnostic.primary_label.span)?;
let related_information = diagnostic
.labels
.secondary_labels
.into_iter()
.map(|label| {
let (file_map, range) = match primary_file_map {
// If the label's span does not point anywhere, assume it comes from the same file
// as the primary label
Some(file_map) if label.span.start() == ByteIndex::none() => {
(file_map, UNKNOWN_RANGE)
},
Some(_) | None => {
let file_map = find_file(label.span.start())?;
let range = byte_span_to_range(file_map, label.span)?;
// If the label's span does not point anywhere, assume it comes from the same file
// as the primary label
let (file_map, range) = if label.span.start() == ByteIndex::none() {
(primary_file_map, UNKNOWN_RANGE)
} else {
let file_map = find_file(label.span.start())?;
let range = byte_span_to_range(file_map, label.span)?;
(file_map, range)
},
(file_map, range)
};
let uri = codespan_name_to_file(file_map.name())
+15 -17
View File
@@ -2,7 +2,7 @@ use structopt::StructOpt;
use codespan::{CodeMap, Span};
use codespan_reporting::termcolor::StandardStream;
use codespan_reporting::{emit, ColorArg, Diagnostic, Label, Severity};
use codespan_reporting::{emit, ColorArg, Diagnostic, Label};
#[derive(Debug, StructOpt)]
#[structopt(name = "emit")]
@@ -31,25 +31,23 @@ fn main() {
let file_map = code_map.add_filemap("test".into(), source.to_string());
let str_start = file_map.byte_index(3.into(), 6.into()).unwrap();
let error = Diagnostic::new(Severity::Error, "Unexpected type in `+` application")
.with_label(
Label::new_primary(Span::from_offset(str_start, 2.into()))
.with_message("Expected integer but got string"),
)
.with_label(
Label::new_secondary(Span::from_offset(str_start, 2.into()))
.with_message("Expected integer but got string"),
)
.with_code("E0001");
let error = Diagnostic::new_error(
"Unexpected type in `+` application",
Label::new(
Span::from_offset(str_start, 2.into()),
"Expected integer but got string",
),
)
.with_code("E0001")
.with_secondary_labels(vec![Label::new(
Span::from_offset(str_start, 2.into()),
"Expected integer but got string",
)]);
let line_start = file_map.byte_index(2.into(), 3.into()).unwrap();
let warning = Diagnostic::new(
Severity::Warning,
let warning = Diagnostic::new_warning(
"`+` function has no effect unless its result is used",
)
.with_label(
Label::new_primary(Span::from_offset(line_start, 27.into()))
.with_message("Value discarded"),
Label::new(Span::from_offset(line_start, 27.into()), "Value discarded"),
);
let diagnostics = [error, warning];
+25 -54
View File
@@ -6,18 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::Severity;
/// A style for the label
#[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "memory_usage", derive(heapsize_derive::HeapSizeOf))]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub enum LabelStyle {
/// The main focus of the diagnostic
Primary,
/// Supporting labels that may help to isolate the cause of the diagnostic
Secondary,
}
/// A label describing an underlined region of code associated with a diagnostic
/// A label describing an underlined region of code associated with a diagnostic.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "memory_usage", derive(heapsize_derive::HeapSizeOf))]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
@@ -26,34 +15,19 @@ pub struct Label {
pub span: ByteSpan,
/// A message to provide some additional information for the underlined code.
pub message: String,
/// The style to use for the label.
pub style: LabelStyle,
}
impl Label {
pub fn new(span: ByteSpan, style: LabelStyle) -> Label {
pub fn new(span: ByteSpan, message: impl Into<String>) -> Label {
Label {
span,
message: String::new(),
style,
message: message.into(),
}
}
pub fn new_primary(span: ByteSpan) -> Label {
Label::new(span, LabelStyle::Primary)
}
pub fn new_secondary(span: ByteSpan) -> Label {
Label::new(span, LabelStyle::Secondary)
}
pub fn with_message(mut self, message: impl Into<String>) -> Label {
self.message = message.into();
self
}
}
/// Represents a diagnostic message and associated child messages.
/// Represents a diagnostic message that can provide information like errors and
/// warnings to the user.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "memory_usage", derive(heapsize_derive::HeapSizeOf))]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
@@ -62,41 +36,43 @@ pub struct Diagnostic {
pub severity: Severity,
/// An optional code that identifies this diagnostic.
pub code: Option<String>,
/// The main message associated with this diagnostic
/// The main message associated with this diagnostic.
pub message: String,
/// The labelled spans marking the regions of code that cause this
/// diagnostic to be raised
pub labels: Vec<Label>,
/// A label that describes the primary cause of this diagnostic.
pub primary_label: Label,
/// Secondary labels that provide additional context for the diagnostic.
pub secondary_labels: Vec<Label>,
}
impl Diagnostic {
pub fn new(severity: Severity, message: impl Into<String>) -> Diagnostic {
pub fn new(severity: Severity, message: impl Into<String>, primary_label: Label) -> Diagnostic {
Diagnostic {
severity,
code: None,
message: message.into(),
labels: Vec::new(),
primary_label,
secondary_labels: Vec::new(),
}
}
pub fn new_bug(message: impl Into<String>) -> Diagnostic {
Diagnostic::new(Severity::Bug, message)
pub fn new_bug(message: impl Into<String>, primary_label: Label) -> Diagnostic {
Diagnostic::new(Severity::Bug, message, primary_label)
}
pub fn new_error(message: impl Into<String>) -> Diagnostic {
Diagnostic::new(Severity::Error, message)
pub fn new_error(message: impl Into<String>, primary_label: Label) -> Diagnostic {
Diagnostic::new(Severity::Error, message, primary_label)
}
pub fn new_warning(message: impl Into<String>) -> Diagnostic {
Diagnostic::new(Severity::Warning, message)
pub fn new_warning(message: impl Into<String>, primary_label: Label) -> Diagnostic {
Diagnostic::new(Severity::Warning, message, primary_label)
}
pub fn new_note(message: impl Into<String>) -> Diagnostic {
Diagnostic::new(Severity::Note, message)
pub fn new_note(message: impl Into<String>, primary_label: Label) -> Diagnostic {
Diagnostic::new(Severity::Note, message, primary_label)
}
pub fn new_help(message: impl Into<String>) -> Diagnostic {
Diagnostic::new(Severity::Help, message)
pub fn new_help(message: impl Into<String>, primary_label: Label) -> Diagnostic {
Diagnostic::new(Severity::Help, message, primary_label)
}
pub fn with_code(mut self, code: impl Into<String>) -> Diagnostic {
@@ -104,13 +80,8 @@ impl Diagnostic {
self
}
pub fn with_label(mut self, label: Label) -> Diagnostic {
self.labels.push(label);
self
}
pub fn with_labels(mut self, labels: impl IntoIterator<Item = Label>) -> Diagnostic {
self.labels.extend(labels);
pub fn with_secondary_labels(mut self, labels: impl IntoIterator<Item = Label>) -> Diagnostic {
self.secondary_labels.extend(labels);
self
}
}
+326 -225
View File
@@ -1,19 +1,31 @@
use codespan::{CodeMap, LineIndex, RawIndex};
use codespan::{ByteIndex, CodeMap, FileMap, LineIndex, RawIndex};
use std::io;
use termcolor::{Color, ColorSpec, WriteColor};
use crate::{Diagnostic, LabelStyle, Severity};
use crate::{Diagnostic, Label, Severity};
/// Configures how a diagnostic is rendered.
#[derive(Clone, Debug)]
pub struct Config {
/// The color to use when rendering bugs. Defaults to `Color::Red`.
pub bug_color: Color,
/// The color to use when rendering errors. Defaults to `Color::Red`.
pub error_color: Color,
/// The color to use when rendering warnings. Defaults to `Color::Yellow`.
pub warning_color: Color,
/// The color to use when rendering notes. Defaults to `Color::Green`.
pub note_color: Color,
/// The color to use when rendering helps. Defaults to `Color::Cyan`.
pub help_color: Color,
/// The color to use when rendering secondary labels. Defaults to
/// `Color::Blue` (or `Color::Cyan` on windows).
pub secondary_color: Color,
/// The color to use when rendering gutters. Defaults to `Color::Blue`
/// (or `Color::Cyan` on windows).
pub gutter_color: Color,
/// The character to use when underlining a primary label. Defaults to: `^`.
pub primary_mark: char,
/// The character to use when underlining a secondary label. Defaults to: `-`.
pub secondary_mark: char,
}
@@ -50,22 +62,6 @@ impl Config {
Severity::Help => self.help_color,
}
}
/// The style used to mark the labelled section of code.
pub fn label_color(&self, severity: Severity, label_style: LabelStyle) -> Color {
match label_style {
LabelStyle::Primary => self.severity_color(severity),
LabelStyle::Secondary => self.secondary_color,
}
}
/// The character used for the underlined section of code.
fn underline_char(&self, label_style: LabelStyle) -> char {
match label_style {
LabelStyle::Primary => self.primary_mark,
LabelStyle::Secondary => self.secondary_mark,
}
}
}
pub fn emit(
@@ -74,221 +70,326 @@ pub fn emit(
codemap: &CodeMap<impl AsRef<str>>,
diagnostic: &Diagnostic,
) -> io::Result<()> {
let severity_color = config.severity_color(diagnostic.severity);
let gutter_spec = ColorSpec::new().set_fg(Some(config.gutter_color)).clone();
let header_message_spec = ColorSpec::new().set_bold(true).set_intense(true).clone();
let header_primary_spec = ColorSpec::new()
.set_bold(true)
.set_intense(true)
.set_fg(Some(severity_color))
.clone();
Header::new(diagnostic).emit(&mut writer, config)?;
// Diagnostic header
//
// ```
// error[E0001]: Unexpected type in `+` application
// ```
// Write severity name
//
// ```
// error
// ```
writer.set_color(&header_primary_spec)?;
write!(writer, "{}", severity_name(diagnostic.severity))?;
if let Some(code) = &diagnostic.code {
// Write error code
//
// ```
// [E0001]
// ```
write!(writer, "[{}]", code)?;
match codemap.find_file(diagnostic.primary_label.span.start()) {
None => SimpleMessage::new(&diagnostic.primary_label).emit(&mut writer, config)?,
Some(file) => MarkedSource::new_primary(file, &diagnostic).emit(&mut writer, config)?,
}
// Write diagnostic message
//
// ```
// : Unexpected type in `+` application
// ```
writer.set_color(&header_message_spec)?;
write!(writer, ": {}", diagnostic.message)?;
write!(writer, "\n")?;
writer.reset()?;
for label in &diagnostic.labels {
for label in &diagnostic.secondary_labels {
match codemap.find_file(label.span.start()) {
None => {
if !label.message.is_empty() {
write!(writer, "- {}", label.message)?;
write!(writer, "\n")?;
}
},
Some(file) => {
let (start_line, start_column) =
file.location(label.span.start()).expect("location_start");
let (end_line, _) = file.location(label.span.end()).expect("location_end");
let start_line_span = file.line_span(start_line).expect("line_span");
let end_line_span = file.line_span(end_line).expect("line_span");
// Use the length of the last line number as the gutter padding
let gutter_padding = format!("{}", end_line.number()).len();
// File name
//
// ```
// ┌╴ <test>:2:9
// ```
// Write gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} ┌╴ ", "", width = gutter_padding)?;
writer.reset()?;
// Write file name
write!(
writer,
"{file}:{line}:{column}",
file = file.name(),
line = start_line.number(),
column = start_column.number(),
)?;
write!(writer, "\n")?;
// Source code snippet
//
// ```
// │
// 2 │ (+ test "")
// │ ^^ Expected integer but got string
// ╵
// ```
// Write line number and gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} │ ", "", width = gutter_padding)?;
write!(writer, "\n")?;
write!(
writer,
"{: >width$} │ ",
start_line.number(),
width = gutter_padding,
)?;
writer.reset()?;
// Write source prefix before marked section
let source_prefix = file
.src_slice(start_line_span.with_end(label.span.start()))
.expect("prefix");
write!(writer, "{}", source_prefix)?;
let label_color = config.label_color(diagnostic.severity, label.style);
let label_spec = ColorSpec::new().set_fg(Some(label_color)).clone();
// Write marked section
let mark_len = if start_line == end_line {
// Single line
// Write marked source section
let marked_source = file.src_slice(label.span).expect("marked_source");
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source)?;
writer.reset()?;
marked_source.len()
} else {
// Multiple lines
// Write marked source section
let marked_source = file
.src_slice(start_line_span.with_start(label.span.start()))
.expect("start_of_marked");
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source)?;
for line_index in ((start_line.to_usize() + 1)..end_line.to_usize())
.map(|i| LineIndex::from(i as RawIndex))
{
// Write line number and gutter
writer.set_color(&gutter_spec)?;
write!(
writer,
"{: >width$} │ ",
line_index.number(),
width = gutter_padding,
)?;
// Write marked source section
let line_span = file.line_span(line_index).expect("marked_line_span");
let marked_source = file
.src_slice(line_span)
.expect("marked_source")
.trim_end_matches(|ch: char| ch == '\r' || ch == '\n');
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source)?;
write!(writer, "\n")?;
}
// Write line number and gutter
writer.set_color(&gutter_spec)?;
write!(
writer,
"{: >width$} │ ",
end_line.number(),
width = gutter_padding,
)?;
// Write marked source section
let marked_source = file
.src_slice(end_line_span.with_end(label.span.end()))
.expect("marked_source");
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source)?;
writer.reset()?;
marked_source.len()
};
// Write source suffix after marked section
let source = file
.src_slice(end_line_span.with_start(label.span.end()))
.expect("suffix")
.trim_end_matches(|ch: char| ch == '\r' || ch == '\n');
write!(writer, "{}", source)?;
write!(writer, "\n")?;
// Write underline gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} │ ", "", width = gutter_padding)?;
writer.reset()?;
// Write underline and label
writer.set_color(&label_spec)?;
write!(writer, "{: >width$}", "", width = source_prefix.len())?;
for _ in 0..mark_len {
write!(writer, "{}", config.underline_char(label.style))?;
}
if !label.message.is_empty() {
write!(writer, " {}", label.message)?;
write!(writer, "\n")?;
}
writer.reset()?;
// Write final gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} ╵", "", width = gutter_padding)?;
write!(writer, "\n")?;
writer.reset()?;
},
None => SimpleMessage::new(&label).emit(&mut writer, config)?,
Some(file) => MarkedSource::new_secondary(file, &label).emit(&mut writer, config)?,
}
}
Ok(())
}
/// A string that explains this diagnostic severity.
fn severity_name(severity: Severity) -> &'static str {
match severity {
Severity::Bug => "bug",
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Note => "note",
Severity::Help => "help",
/// Diagnostic header
///
/// ```text
/// error[E0001]: Unexpected type in `+` application
/// ```
#[derive(Copy, Clone, Debug)]
struct Header<'a> {
severity: Severity,
code: Option<&'a str>,
message: &'a str,
}
impl<'a> Header<'a> {
fn new(diagnostic: &'a Diagnostic) -> Header<'a> {
Header {
severity: diagnostic.severity,
code: diagnostic.code.as_ref().map(String::as_str),
message: &diagnostic.message,
}
}
fn severity_name(&self) -> &'static str {
match self.severity {
Severity::Bug => "bug",
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Help => "help",
Severity::Note => "note",
}
}
fn emit(&self, writer: &mut impl WriteColor, config: &Config) -> io::Result<()> {
let message_spec = ColorSpec::new().set_bold(true).set_intense(true).clone();
let primary_spec = ColorSpec::new()
.set_bold(true)
.set_intense(true)
.set_fg(Some(config.severity_color(self.severity)))
.clone();
// Write severity name
//
// ```
// error
// ```
writer.set_color(&primary_spec)?;
write!(writer, "{}", self.severity_name())?;
if let Some(code) = &self.code {
// Write error code
//
// ```
// [E0001]
// ```
write!(writer, "[{}]", code)?;
}
// Write diagnostic message
//
// ```
// : Unexpected type in `+` application
// ```
writer.set_color(&message_spec)?;
write!(writer, ": {}", self.message)?;
write!(writer, "\n")?;
writer.reset()?;
Ok(())
}
}
/// A simple message
///
/// ```text
/// - Expected integer but got string
/// ```
struct SimpleMessage<'a> {
message: &'a str,
}
impl<'a> SimpleMessage<'a> {
fn new(label: &'a Label) -> SimpleMessage<'a> {
SimpleMessage {
message: &label.message,
}
}
fn emit(&self, writer: &mut impl WriteColor, _config: &Config) -> io::Result<()> {
if !self.message.is_empty() {
write!(writer, "- {}", self.message)?;
write!(writer, "\n")?;
}
Ok(())
}
}
enum MarkStyle {
Primary(Severity),
Secondary,
}
/// A marked section of source code
///
/// ```text
/// ┌╴ <test>:2:9
/// │
/// 2 │ (+ test "")
/// │ ^^ Expected integer but got string
/// ╵
/// ```
struct MarkedSource<'a, S: AsRef<str>> {
file: &'a FileMap<S>,
label: &'a Label,
mark_style: MarkStyle,
}
impl<'a, S: AsRef<str>> MarkedSource<'a, S> {
fn new_primary(file: &'a FileMap<S>, diagnostic: &'a Diagnostic) -> MarkedSource<'a, S> {
MarkedSource {
file,
label: &diagnostic.primary_label,
mark_style: MarkStyle::Primary(diagnostic.severity),
}
}
fn new_secondary(file: &'a FileMap<S>, label: &'a Label) -> MarkedSource<'a, S> {
MarkedSource {
file,
label,
mark_style: MarkStyle::Secondary,
}
}
fn start(&self) -> ByteIndex {
self.label.span.start()
}
fn end(&self) -> ByteIndex {
self.label.span.end()
}
fn label_color(&self, config: &Config) -> Color {
match self.mark_style {
MarkStyle::Primary(severity) => config.severity_color(severity),
MarkStyle::Secondary => config.secondary_color,
}
}
fn underline_char(&self, config: &Config) -> char {
match self.mark_style {
MarkStyle::Primary(_) => config.primary_mark,
MarkStyle::Secondary => config.secondary_mark,
}
}
fn emit(&self, writer: &mut impl WriteColor, config: &Config) -> io::Result<()> {
let gutter_spec = ColorSpec::new().set_fg(Some(config.gutter_color)).clone();
let label_spec = ColorSpec::new()
.set_fg(Some(self.label_color(config)))
.clone();
let (start_line, start_column) = self.file.location(self.start()).expect("location_start");
let (end_line, _) = self.file.location(self.end()).expect("location_end");
let start_line_span = self.file.line_span(start_line).expect("line_span");
let end_line_span = self.file.line_span(end_line).expect("line_span");
// Use the length of the last line number as the gutter padding
let gutter_padding = format!("{}", end_line.number()).len();
// File name
//
// ```
// ┌╴ <test>:2:9
// ```
// Write gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} ┌╴ ", "", width = gutter_padding)?;
writer.reset()?;
// Write file name
write!(
writer,
"{file}:{line}:{column}",
file = self.file.name(),
line = start_line.number(),
column = start_column.number(),
)?;
write!(writer, "\n")?;
// Source code snippet
//
// ```
// │
// 2 │ (+ test "")
// │ ^^ Expected integer but got string
// ╵
// ```
// Write line number and gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} │ ", "", width = gutter_padding)?;
write!(writer, "\n")?;
write!(
writer,
"{: >width$} │ ",
start_line.number(),
width = gutter_padding,
)?;
writer.reset()?;
let line_trimmer = |ch: char| ch == '\r' || ch == '\n';
// Write source prefix before marked section
let prefix_span = start_line_span.with_end(self.start());
let source_prefix = self.file.src_slice(prefix_span).expect("prefix");
write!(writer, "{}", source_prefix)?;
// Write marked section
let mark_len = if start_line == end_line {
// Single line
// Write marked source section
let marked_source = self.file.src_slice(self.label.span).expect("marked_source");
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source)?;
writer.reset()?;
marked_source.len()
} else {
// Multiple lines
// Write marked source section
let marked_span = start_line_span.with_start(self.start());
let marked_source = self.file.src_slice(marked_span).expect("start_of_marked");
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source)?;
for line_index in ((start_line.to_usize() + 1)..end_line.to_usize())
.map(|i| LineIndex::from(i as RawIndex))
{
// Write line number and gutter
writer.set_color(&gutter_spec)?;
write!(
writer,
"{: >width$} │ ",
line_index.number(),
width = gutter_padding,
)?;
// Write marked source section
let mark_span = self.file.line_span(line_index).expect("marked_line_span");
let marked_source = self.file.src_slice(mark_span).expect("marked_source");
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source.trim_end_matches(line_trimmer))?;
write!(writer, "\n")?;
}
// Write line number and gutter
writer.set_color(&gutter_spec)?;
write!(
writer,
"{: >width$} │ ",
end_line.number(),
width = gutter_padding,
)?;
// Write marked source section
let mark_span = end_line_span.with_end(self.end());
let marked_source = self.file.src_slice(mark_span).expect("marked_source");
writer.set_color(&label_spec)?;
write!(writer, "{}", marked_source)?;
writer.reset()?;
marked_source.len()
};
// Write source suffix after marked section
let suffix_span = end_line_span.with_start(self.end());
let source_suffix = self.file.src_slice(suffix_span).expect("suffix");
write!(writer, "{}", source_suffix.trim_end_matches(line_trimmer))?;
write!(writer, "\n")?;
// Write underline gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} │ ", "", width = gutter_padding)?;
writer.reset()?;
// Write underline and label
writer.set_color(&label_spec)?;
write!(writer, "{: >width$}", "", width = source_prefix.len())?;
for _ in 0..mark_len {
write!(writer, "{}", self.underline_char(config))?;
}
if !self.label.message.is_empty() {
write!(writer, " {}", self.label.message)?;
write!(writer, "\n")?;
}
writer.reset()?;
// Write final gutter
writer.set_color(&gutter_spec)?;
write!(writer, "{: >width$} ╵", "", width = gutter_padding)?;
write!(writer, "\n")?;
writer.reset()?;
Ok(())
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ mod emitter;
pub use termcolor;
pub use self::diagnostic::{Diagnostic, Label, LabelStyle};
pub use self::diagnostic::{Diagnostic, Label};
pub use self::emitter::{emit, Config};
/// A severity level for diagnostic messages