mirror of https://github.com/astral-sh/ruff
259 lines
7.4 KiB
Rust
259 lines
7.4 KiB
Rust
use std::fmt::{Display, Formatter};
|
|
use std::io::Write;
|
|
use std::num::NonZeroUsize;
|
|
|
|
use colored::Colorize;
|
|
|
|
use ruff_notebook::NotebookIndex;
|
|
use ruff_source_file::OneIndexed;
|
|
|
|
use crate::fs::relativize_path;
|
|
use crate::message::diff::calculate_print_width;
|
|
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
|
|
use crate::message::{
|
|
Emitter, EmitterContext, Message, MessageWithLocation, group_messages_by_filename,
|
|
};
|
|
use crate::settings::types::UnsafeFixes;
|
|
|
|
#[derive(Default)]
|
|
pub struct GroupedEmitter {
|
|
show_fix_status: bool,
|
|
show_source: bool,
|
|
unsafe_fixes: UnsafeFixes,
|
|
}
|
|
|
|
impl GroupedEmitter {
|
|
#[must_use]
|
|
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
|
|
self.show_fix_status = show_fix_status;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_show_source(mut self, show_source: bool) -> Self {
|
|
self.show_source = show_source;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
|
|
self.unsafe_fixes = unsafe_fixes;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Emitter for GroupedEmitter {
|
|
fn emit(
|
|
&mut self,
|
|
writer: &mut dyn Write,
|
|
messages: &[Message],
|
|
context: &EmitterContext,
|
|
) -> anyhow::Result<()> {
|
|
for (filename, messages) in group_messages_by_filename(messages) {
|
|
// Compute the maximum number of digits in the row and column, for messages in
|
|
// this file.
|
|
|
|
let mut max_row_length = OneIndexed::MIN;
|
|
let mut max_column_length = OneIndexed::MIN;
|
|
|
|
for message in &messages {
|
|
max_row_length = max_row_length.max(message.start_location.line);
|
|
max_column_length = max_column_length.max(message.start_location.column);
|
|
}
|
|
|
|
let row_length = calculate_print_width(max_row_length);
|
|
let column_length = calculate_print_width(max_column_length);
|
|
|
|
// Print the filename.
|
|
writeln!(writer, "{}:", relativize_path(&*filename).underline())?;
|
|
|
|
// Print each message.
|
|
for message in messages {
|
|
write!(
|
|
writer,
|
|
"{}",
|
|
DisplayGroupedMessage {
|
|
notebook_index: context.notebook_index(&message.filename()),
|
|
message,
|
|
show_fix_status: self.show_fix_status,
|
|
unsafe_fixes: self.unsafe_fixes,
|
|
show_source: self.show_source,
|
|
row_length,
|
|
column_length,
|
|
}
|
|
)?;
|
|
}
|
|
|
|
// Print a blank line between files, unless we're showing the source, in which case
|
|
// we'll have already printed a blank line between messages.
|
|
if !self.show_source {
|
|
writeln!(writer)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct DisplayGroupedMessage<'a> {
|
|
message: MessageWithLocation<'a>,
|
|
show_fix_status: bool,
|
|
unsafe_fixes: UnsafeFixes,
|
|
show_source: bool,
|
|
row_length: NonZeroUsize,
|
|
column_length: NonZeroUsize,
|
|
notebook_index: Option<&'a NotebookIndex>,
|
|
}
|
|
|
|
impl Display for DisplayGroupedMessage<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
let MessageWithLocation {
|
|
message,
|
|
start_location,
|
|
} = &self.message;
|
|
|
|
write!(
|
|
f,
|
|
" {row_padding}",
|
|
row_padding = " "
|
|
.repeat(self.row_length.get() - calculate_print_width(start_location.line).get())
|
|
)?;
|
|
|
|
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
|
let (row, col) = if let Some(jupyter_index) = self.notebook_index {
|
|
write!(
|
|
f,
|
|
"cell {cell}{sep}",
|
|
cell = jupyter_index
|
|
.cell(start_location.line)
|
|
.unwrap_or(OneIndexed::MIN),
|
|
sep = ":".cyan()
|
|
)?;
|
|
(
|
|
jupyter_index
|
|
.cell_row(start_location.line)
|
|
.unwrap_or(OneIndexed::MIN),
|
|
start_location.column,
|
|
)
|
|
} else {
|
|
(start_location.line, start_location.column)
|
|
};
|
|
|
|
writeln!(
|
|
f,
|
|
"{row}{sep}{col}{col_padding} {code_and_body}",
|
|
sep = ":".cyan(),
|
|
col_padding = " ".repeat(
|
|
self.column_length.get() - calculate_print_width(start_location.column).get()
|
|
),
|
|
code_and_body = RuleCodeAndBody {
|
|
message,
|
|
show_fix_status: self.show_fix_status,
|
|
unsafe_fixes: self.unsafe_fixes
|
|
},
|
|
)?;
|
|
|
|
if self.show_source {
|
|
use std::fmt::Write;
|
|
let mut padded = PadAdapter::new(f);
|
|
writeln!(
|
|
padded,
|
|
"{}",
|
|
MessageCodeFrame {
|
|
message,
|
|
notebook_index: self.notebook_index
|
|
}
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Adapter that adds a ' ' at the start of every line without the need to copy the string.
|
|
/// Inspired by Rust's `debug_struct()` internal implementation that also uses a `PadAdapter`.
|
|
struct PadAdapter<'buf> {
|
|
buf: &'buf mut (dyn std::fmt::Write + 'buf),
|
|
on_newline: bool,
|
|
}
|
|
|
|
impl<'buf> PadAdapter<'buf> {
|
|
fn new(buf: &'buf mut (dyn std::fmt::Write + 'buf)) -> Self {
|
|
Self {
|
|
buf,
|
|
on_newline: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Write for PadAdapter<'_> {
|
|
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
|
for s in s.split_inclusive('\n') {
|
|
if self.on_newline {
|
|
self.buf.write_str(" ")?;
|
|
}
|
|
|
|
self.on_newline = s.ends_with('\n');
|
|
self.buf.write_str(s)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_snapshot;
|
|
|
|
use crate::message::GroupedEmitter;
|
|
use crate::message::tests::{
|
|
capture_emitter_output, create_messages, create_syntax_error_messages,
|
|
};
|
|
use crate::settings::types::UnsafeFixes;
|
|
|
|
#[test]
|
|
fn default() {
|
|
let mut emitter = GroupedEmitter::default();
|
|
let content = capture_emitter_output(&mut emitter, &create_messages());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn syntax_errors() {
|
|
let mut emitter = GroupedEmitter::default();
|
|
let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn show_source() {
|
|
let mut emitter = GroupedEmitter::default().with_show_source(true);
|
|
let content = capture_emitter_output(&mut emitter, &create_messages());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn fix_status() {
|
|
let mut emitter = GroupedEmitter::default()
|
|
.with_show_fix_status(true)
|
|
.with_show_source(true);
|
|
let content = capture_emitter_output(&mut emitter, &create_messages());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn fix_status_unsafe() {
|
|
let mut emitter = GroupedEmitter::default()
|
|
.with_show_fix_status(true)
|
|
.with_show_source(true)
|
|
.with_unsafe_fixes(UnsafeFixes::Enabled);
|
|
let content = capture_emitter_output(&mut emitter, &create_messages());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
}
|