Use annotate-snippets for emitting errors (#3507)
This commit is contained in:
parent
efa3a62b56
commit
3dc625c661
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -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"
|
||||
|
@ -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`
|
||||
|
@ -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()
|
||||
|
173
src/format_report_formatter.rs
Normal file
173
src/format_report_formatter.rs
Normal 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,
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
124
src/lib.rs
124
src/lib.rs
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user