Use annotate-snippets for emitting errors (#3507)

This commit is contained in:
Ruben Schmidmeister 2019-04-17 14:33:36 +02:00 committed by Seiichi Uchida
parent efa3a62b56
commit 3dc625c661
8 changed files with 238 additions and 136 deletions

19
Cargo.lock generated
View File

@ -8,6 +8,22 @@ dependencies = [
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "annotate-snippets"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "argon2rs"
version = "0.2.5"
@ -744,6 +760,7 @@ dependencies = [
name = "rustfmt-nightly"
version = "1.2.0"
dependencies = [
"annotate-snippets 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"bytecount 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cargo_metadata 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -991,6 +1008,8 @@ dependencies = [
[metadata]
"checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
"checksum annotate-snippets 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e8bcdcd5b291ce85a78f2b9d082a8de9676c12b1840d386d67bc5eea6f9d2b4e"
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392"
"checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71"
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"

View File

@ -58,6 +58,7 @@ unicode-width = "0.1.5"
unicode_categories = "0.1.1"
dirs = "1.0.4"
ignore = "0.4.6"
annotate-snippets = { version = "0.5.0", features = ["ansi_term"] }
# A noop dependency that changes in the Rust repository, it's a bit of a hack.
# See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust`

View File

@ -16,7 +16,7 @@ use getopts::{Matches, Options};
use crate::rustfmt::{
load_config, CliOptions, Color, Config, Edition, EmitMode, ErrorKind, FileLines, FileName,
Input, Session, Verbosity,
FormatReportFormatterBuilder, Input, Session, Verbosity,
};
fn main() {
@ -310,19 +310,12 @@ fn format_and_emit_report<T: Write>(session: &mut Session<'_, T>, input: Input)
match session.format(input) {
Ok(report) => {
if report.has_warnings() {
match term::stderr() {
Some(ref t)
if session.config.color().use_colored_tty()
&& t.supports_color()
&& t.supports_attr(term::Attr::Bold) =>
{
match report.fancy_print(term::stderr().unwrap()) {
Ok(..) => (),
Err(..) => panic!("Unable to write to stderr: {}", report),
}
}
_ => eprintln!("{}", report),
}
eprintln!(
"{}",
FormatReportFormatterBuilder::new(&report)
.enable_colors(should_print_with_colors(session))
.build()
);
}
}
Err(msg) => {
@ -332,6 +325,19 @@ fn format_and_emit_report<T: Write>(session: &mut Session<'_, T>, input: Input)
}
}
fn should_print_with_colors<T: Write>(session: &mut Session<'_, T>) -> bool {
match term::stderr() {
Some(ref t)
if session.config.color().use_colored_tty()
&& t.supports_color()
&& t.supports_attr(term::Attr::Bold) =>
{
true
}
_ => false,
}
}
fn print_usage_to_stdout(opts: &Options, reason: &str) {
let sep = if reason.is_empty() {
String::new()

View File

@ -0,0 +1,173 @@
use crate::config::FileName;
use crate::formatting::FormattingError;
use crate::{ErrorKind, FormatReport};
use annotate_snippets::display_list::DisplayList;
use annotate_snippets::formatter::DisplayListFormatter;
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use std::fmt::{self, Display};
/// A builder for [`FormatReportFormatter`].
pub struct FormatReportFormatterBuilder<'a> {
report: &'a FormatReport,
enable_colors: bool,
}
impl<'a> FormatReportFormatterBuilder<'a> {
/// Creates a new [`FormatReportFormatterBuilder`].
pub fn new(report: &'a FormatReport) -> Self {
Self {
report,
enable_colors: false,
}
}
/// Enables colors and formatting in the output.
pub fn enable_colors(self, enable_colors: bool) -> Self {
Self {
enable_colors,
..self
}
}
/// Creates a new [`FormatReportFormatter`] from the settings in this builder.
pub fn build(self) -> FormatReportFormatter<'a> {
FormatReportFormatter {
report: self.report,
enable_colors: self.enable_colors,
}
}
}
/// Formats the warnings/errors in a [`FormatReport`].
///
/// Can be created using a [`FormatReportFormatterBuilder`].
pub struct FormatReportFormatter<'a> {
report: &'a FormatReport,
enable_colors: bool,
}
impl<'a> Display for FormatReportFormatter<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let formatter = DisplayListFormatter::new(self.enable_colors);
let errors_by_file = &self.report.internal.borrow().0;
for (file, errors) in errors_by_file {
for error in errors {
let snippet = formatting_error_to_snippet(file, error);
writeln!(f, "{}\n", formatter.format(&DisplayList::from(snippet)))?;
}
}
if !errors_by_file.is_empty() {
let snippet = formatting_failure_snippet(self.report.warning_count());
writeln!(f, "{}", formatter.format(&DisplayList::from(snippet)))?;
}
Ok(())
}
}
fn formatting_failure_snippet(warning_count: usize) -> Snippet {
Snippet {
title: Some(Annotation {
id: None,
label: Some(format!(
"rustfmt has failed to format. See previous {} errors.",
warning_count
)),
annotation_type: AnnotationType::Warning,
}),
footer: Vec::new(),
slices: Vec::new(),
}
}
fn formatting_error_to_snippet(file: &FileName, error: &FormattingError) -> Snippet {
let slices = vec![snippet_code_slice(file, error)];
let title = Some(snippet_title(error));
let footer = snippet_footer(error).into_iter().collect();
Snippet {
title,
footer,
slices,
}
}
fn snippet_title(error: &FormattingError) -> Annotation {
let annotation_type = error_kind_to_snippet_annotation_type(&error.kind);
Annotation {
id: title_annotation_id(error),
label: Some(error.kind.to_string()),
annotation_type,
}
}
fn snippet_footer(error: &FormattingError) -> Option<Annotation> {
let message_suffix = error.msg_suffix();
if !message_suffix.is_empty() {
Some(Annotation {
id: None,
label: Some(message_suffix.to_string()),
annotation_type: AnnotationType::Note,
})
} else {
None
}
}
fn snippet_code_slice(file: &FileName, error: &FormattingError) -> Slice {
let annotations = slice_annotation(error).into_iter().collect();
let origin = Some(format!("{}:{}", file, error.line));
let source = error.line_buffer.clone();
Slice {
source,
line_start: error.line,
origin,
fold: false,
annotations,
}
}
fn slice_annotation(error: &FormattingError) -> Option<SourceAnnotation> {
let (range_start, range_length) = error.format_len();
let range_end = range_start + range_length;
if range_length > 0 {
Some(SourceAnnotation {
annotation_type: AnnotationType::Error,
range: (range_start, range_end),
label: String::new(),
})
} else {
None
}
}
fn title_annotation_id(error: &FormattingError) -> Option<String> {
const INTERNAL_ERROR_ID: &str = "internal";
if error.is_internal() {
Some(INTERNAL_ERROR_ID.to_string())
} else {
None
}
}
fn error_kind_to_snippet_annotation_type(error_kind: &ErrorKind) -> AnnotationType {
match error_kind {
ErrorKind::LineOverflow(..)
| ErrorKind::TrailingWhitespace
| ErrorKind::IoError(_)
| ErrorKind::ParseError
| ErrorKind::LostComment
| ErrorKind::LicenseCheck
| ErrorKind::BadAttr
| ErrorKind::InvalidGlobPattern(_)
| ErrorKind::VersionMismatch => AnnotationType::Error,
ErrorKind::BadIssue(_) | ErrorKind::DeprecatedAttr => AnnotationType::Warning,
}
}

View File

@ -275,18 +275,14 @@ impl FormattingError {
}
}
pub(crate) fn msg_prefix(&self) -> &str {
pub(crate) fn is_internal(&self) -> bool {
match self.kind {
ErrorKind::LineOverflow(..)
| ErrorKind::TrailingWhitespace
| ErrorKind::IoError(_)
| ErrorKind::ParseError
| ErrorKind::LostComment => "internal error:",
ErrorKind::LicenseCheck
| ErrorKind::BadAttr
| ErrorKind::InvalidGlobPattern(..)
| ErrorKind::VersionMismatch => "error:",
ErrorKind::BadIssue(_) | ErrorKind::DeprecatedAttr => "warning:",
| ErrorKind::LostComment => true,
_ => false,
}
}

View File

@ -11,7 +11,7 @@ use env_logger;
use getopts::{Matches, Options};
use rustfmt_nightly as rustfmt;
use crate::rustfmt::{load_config, CliOptions, Input, Session};
use crate::rustfmt::{load_config, CliOptions, FormatReportFormatterBuilder, Input, Session};
fn prune_files(files: Vec<&str>) -> Vec<&str> {
let prefixes: Vec<_> = files
@ -67,7 +67,7 @@ fn fmt_files(files: &[&str]) -> i32 {
for file in files {
let report = session.format(Input::File(PathBuf::from(file))).unwrap();
if report.has_warnings() {
eprintln!("{}", report);
eprintln!("{}", FormatReportFormatterBuilder::new(&report).build());
}
if !session.has_no_errors() {
exit_code = 1;

View File

@ -34,6 +34,8 @@ pub use crate::config::{
Range, Verbosity,
};
pub use crate::format_report_formatter::{FormatReportFormatter, FormatReportFormatterBuilder};
pub use crate::rustfmt_diff::{ModifiedChunk, ModifiedLines};
#[macro_use]
@ -46,6 +48,7 @@ mod closures;
mod comment;
pub(crate) mod config;
mod expr;
mod format_report_formatter;
pub(crate) mod formatting;
mod ignore_path;
mod imports;
@ -162,7 +165,7 @@ impl FormattedSnippet {
/// Reports on any issues that occurred during a run of Rustfmt.
///
/// Can be reported to the user via its `Display` implementation of `print_fancy`.
/// Can be reported to the user using the `Display` impl on [`FormatReportFormatter`].
#[derive(Clone)]
pub struct FormatReport {
// Maps stringified file paths to their associated formatting errors.
@ -245,126 +248,27 @@ impl FormatReport {
/// Print the report to a terminal using colours and potentially other
/// fancy output.
#[deprecated(note = "Use FormatReportFormatter with colors enabled instead")]
pub fn fancy_print(
&self,
mut t: Box<dyn term::Terminal<Output = io::Stderr>>,
) -> Result<(), term::Error> {
for (file, errors) in &self.internal.borrow().0 {
for error in errors {
let prefix_space_len = error.line.to_string().len();
let prefix_spaces = " ".repeat(1 + prefix_space_len);
// First line: the overview of error
t.fg(term::color::RED)?;
t.attr(term::Attr::Bold)?;
write!(t, "{} ", error.msg_prefix())?;
t.reset()?;
t.attr(term::Attr::Bold)?;
writeln!(t, "{}", error.kind)?;
// Second line: file info
write!(t, "{}--> ", &prefix_spaces[1..])?;
t.reset()?;
writeln!(t, "{}:{}", file, error.line)?;
// Third to fifth lines: show the line which triggered error, if available.
if !error.line_buffer.is_empty() {
let (space_len, target_len) = error.format_len();
t.attr(term::Attr::Bold)?;
write!(t, "{}|\n{} | ", prefix_spaces, error.line)?;
t.reset()?;
writeln!(t, "{}", error.line_buffer)?;
t.attr(term::Attr::Bold)?;
write!(t, "{}| ", prefix_spaces)?;
t.fg(term::color::RED)?;
writeln!(t, "{}", FormatReport::target_str(space_len, target_len))?;
t.reset()?;
}
// The last line: show note if available.
let msg_suffix = error.msg_suffix();
if !msg_suffix.is_empty() {
t.attr(term::Attr::Bold)?;
write!(t, "{}= note: ", prefix_spaces)?;
t.reset()?;
writeln!(t, "{}", error.msg_suffix())?;
} else {
writeln!(t)?;
}
t.reset()?;
}
}
if !self.internal.borrow().0.is_empty() {
t.attr(term::Attr::Bold)?;
write!(t, "warning: ")?;
t.reset()?;
write!(
t,
"rustfmt may have failed to format. See previous {} errors.\n\n",
self.warning_count(),
)?;
}
writeln!(
t,
"{}",
FormatReportFormatterBuilder::new(&self)
.enable_colors(true)
.build()
)?;
Ok(())
}
fn target_str(space_len: usize, target_len: usize) -> String {
let empty_line = " ".repeat(space_len);
let overflowed = "^".repeat(target_len);
empty_line + &overflowed
}
}
#[deprecated(note = "Use FormatReportFormatter instead")]
impl fmt::Display for FormatReport {
// Prints all the formatting errors.
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
for (file, errors) in &self.internal.borrow().0 {
for error in errors {
let prefix_space_len = error.line.to_string().len();
let prefix_spaces = " ".repeat(1 + prefix_space_len);
let error_line_buffer = if error.line_buffer.is_empty() {
String::from(" ")
} else {
let (space_len, target_len) = error.format_len();
format!(
"{}|\n{} | {}\n{}| {}",
prefix_spaces,
error.line,
error.line_buffer,
prefix_spaces,
FormatReport::target_str(space_len, target_len)
)
};
let error_info = format!("{} {}", error.msg_prefix(), error.kind);
let file_info = format!("{}--> {}:{}", &prefix_spaces[1..], file, error.line);
let msg_suffix = error.msg_suffix();
let note = if msg_suffix.is_empty() {
String::new()
} else {
format!("{}note= ", prefix_spaces)
};
writeln!(
fmt,
"{}\n{}\n{}\n{}{}",
error_info,
file_info,
error_line_buffer,
note,
error.msg_suffix()
)?;
}
}
if !self.internal.borrow().0.is_empty() {
writeln!(
fmt,
"warning: rustfmt may have failed to format. See previous {} errors.",
self.warning_count(),
)?;
}
write!(fmt, "{}", FormatReportFormatterBuilder::new(&self).build())?;
Ok(())
}
}

View File

@ -13,7 +13,7 @@ use crate::config::{Color, Config, EmitMode, FileName, NewlineStyle, ReportTacti
use crate::formatting::{ReportedErrors, SourceFile};
use crate::rustfmt_diff::{make_diff, print_diff, DiffLine, Mismatch, ModifiedChunk, OutputWriter};
use crate::source_file;
use crate::{FormatReport, Input, Session};
use crate::{FormatReport, FormatReportFormatterBuilder, Input, Session};
const DIFF_CONTEXT_SIZE: usize = 3;
const CONFIGURATIONS_FILE_NAME: &str = "Configurations.md";
@ -299,7 +299,10 @@ fn self_tests() {
assert_eq!(fails, 0, "{} self tests failed", fails);
for format_report in reports {
println!("{}", format_report);
println!(
"{}",
FormatReportFormatterBuilder::new(&format_report).build()
);
warnings += format_report.warning_count();
}
@ -427,7 +430,7 @@ fn check_files(files: Vec<PathBuf>, opt_config: &Option<PathBuf>) -> (Vec<Format
match idempotent_check(&file_name, &opt_config) {
Ok(ref report) if report.has_warnings() => {
print!("{}", report);
print!("{}", FormatReportFormatterBuilder::new(&report).build());
fails += 1;
}
Ok(report) => reports.push(report),