mirror of https://github.com/astral-sh/ruff
269 lines
7.7 KiB
Rust
269 lines
7.7 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::fmt::{Display, Formatter};
|
|
use std::io::Write;
|
|
use std::num::NonZeroUsize;
|
|
|
|
use colored::Colorize;
|
|
|
|
use ruff_db::diagnostic::Diagnostic;
|
|
use ruff_diagnostics::Applicability;
|
|
use ruff_notebook::NotebookIndex;
|
|
use ruff_source_file::{LineColumn, OneIndexed};
|
|
|
|
use crate::fs::relativize_path;
|
|
use crate::message::{Emitter, EmitterContext};
|
|
|
|
pub struct GroupedEmitter {
|
|
show_fix_status: bool,
|
|
applicability: Applicability,
|
|
}
|
|
|
|
impl Default for GroupedEmitter {
|
|
fn default() -> Self {
|
|
Self {
|
|
show_fix_status: false,
|
|
applicability: Applicability::Safe,
|
|
}
|
|
}
|
|
}
|
|
|
|
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_applicability(mut self, applicability: Applicability) -> Self {
|
|
self.applicability = applicability;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Emitter for GroupedEmitter {
|
|
fn emit(
|
|
&mut self,
|
|
writer: &mut dyn Write,
|
|
diagnostics: &[Diagnostic],
|
|
context: &EmitterContext,
|
|
) -> anyhow::Result<()> {
|
|
for (filename, messages) in group_diagnostics_by_filename(diagnostics) {
|
|
// 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 = max_row_length.digits();
|
|
let column_length = max_column_length.digits();
|
|
|
|
// 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.expect_ruff_filename()),
|
|
message,
|
|
show_fix_status: self.show_fix_status,
|
|
applicability: self.applicability,
|
|
row_length,
|
|
column_length,
|
|
}
|
|
)?;
|
|
}
|
|
|
|
// Print a blank line between files.
|
|
writeln!(writer)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct MessageWithLocation<'a> {
|
|
message: &'a Diagnostic,
|
|
start_location: LineColumn,
|
|
}
|
|
|
|
impl std::ops::Deref for MessageWithLocation<'_> {
|
|
type Target = Diagnostic;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.message
|
|
}
|
|
}
|
|
|
|
fn group_diagnostics_by_filename(
|
|
diagnostics: &[Diagnostic],
|
|
) -> BTreeMap<String, Vec<MessageWithLocation<'_>>> {
|
|
let mut grouped_messages = BTreeMap::default();
|
|
for diagnostic in diagnostics {
|
|
grouped_messages
|
|
.entry(diagnostic.expect_ruff_filename())
|
|
.or_insert_with(Vec::new)
|
|
.push(MessageWithLocation {
|
|
message: diagnostic,
|
|
start_location: diagnostic.ruff_start_location().unwrap_or_default(),
|
|
});
|
|
}
|
|
grouped_messages
|
|
}
|
|
|
|
struct DisplayGroupedMessage<'a> {
|
|
message: MessageWithLocation<'a>,
|
|
show_fix_status: bool,
|
|
applicability: Applicability,
|
|
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() - start_location.line.digits().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() - start_location.column.digits().get()),
|
|
code_and_body = RuleCodeAndBody {
|
|
message,
|
|
show_fix_status: self.show_fix_status,
|
|
applicability: self.applicability
|
|
},
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(super) struct RuleCodeAndBody<'a> {
|
|
pub(crate) message: &'a Diagnostic,
|
|
pub(crate) show_fix_status: bool,
|
|
pub(crate) applicability: Applicability,
|
|
}
|
|
|
|
impl Display for RuleCodeAndBody<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
if self.show_fix_status {
|
|
if let Some(fix) = self.message.fix() {
|
|
// Do not display an indicator for inapplicable fixes
|
|
if fix.applies(self.applicability) {
|
|
if let Some(code) = self.message.secondary_code() {
|
|
write!(f, "{} ", code.red().bold())?;
|
|
}
|
|
return write!(
|
|
f,
|
|
"{fix}{body}",
|
|
fix = format_args!("[{}] ", "*".cyan()),
|
|
body = self.message.body(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(code) = self.message.secondary_code() {
|
|
write!(
|
|
f,
|
|
"{code} {body}",
|
|
code = code.red().bold(),
|
|
body = self.message.body(),
|
|
)
|
|
} else {
|
|
write!(
|
|
f,
|
|
"{code}: {body}",
|
|
code = self.message.id().as_str().red().bold(),
|
|
body = self.message.body(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_snapshot;
|
|
|
|
use ruff_diagnostics::Applicability;
|
|
|
|
use crate::message::GroupedEmitter;
|
|
use crate::message::tests::{
|
|
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
|
|
};
|
|
|
|
#[test]
|
|
fn default() {
|
|
let mut emitter = GroupedEmitter::default();
|
|
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn syntax_errors() {
|
|
let mut emitter = GroupedEmitter::default();
|
|
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn fix_status() {
|
|
let mut emitter = GroupedEmitter::default().with_show_fix_status(true);
|
|
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn fix_status_unsafe() {
|
|
let mut emitter = GroupedEmitter::default()
|
|
.with_show_fix_status(true)
|
|
.with_applicability(Applicability::Unsafe);
|
|
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
}
|