mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:35 +00:00
Grouped format implementation (#954)
This commit is contained in:
parent
602291c0c2
commit
ca38c7ac48
4 changed files with 187 additions and 78 deletions
|
@ -236,10 +236,10 @@ See `ruff --help` for more:
|
||||||
```shell
|
```shell
|
||||||
Ruff: An extremely fast Python linter.
|
Ruff: An extremely fast Python linter.
|
||||||
|
|
||||||
Usage: ruff [OPTIONS] <FILES>...
|
Usage: ruff [OPTIONS] [FILES]...
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
<FILES>...
|
[FILES]...
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--config <CONFIG>
|
--config <CONFIG>
|
||||||
|
@ -277,7 +277,7 @@ Options:
|
||||||
--per-file-ignores <PER_FILE_IGNORES>
|
--per-file-ignores <PER_FILE_IGNORES>
|
||||||
List of mappings from file pattern to code to exclude
|
List of mappings from file pattern to code to exclude
|
||||||
--format <FORMAT>
|
--format <FORMAT>
|
||||||
Output serialization format for error messages [default: text] [possible values: text, json]
|
Output serialization format for error messages [default: text] [possible values: text, json, grouped]
|
||||||
--show-source
|
--show-source
|
||||||
Show violations with source code
|
Show violations with source code
|
||||||
--show-files
|
--show-files
|
||||||
|
@ -296,6 +296,8 @@ Options:
|
||||||
Max McCabe complexity allowed for a function
|
Max McCabe complexity allowed for a function
|
||||||
--stdin-filename <STDIN_FILENAME>
|
--stdin-filename <STDIN_FILENAME>
|
||||||
The name of the file when passing it through stdin
|
The name of the file when passing it through stdin
|
||||||
|
--explain <EXPLAIN>
|
||||||
|
Explain a rule
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help information
|
Print help information
|
||||||
-V, --version
|
-V, --version
|
||||||
|
|
|
@ -43,7 +43,7 @@ struct Explanation<'a> {
|
||||||
/// Explain a `CheckCode` to the user.
|
/// Explain a `CheckCode` to the user.
|
||||||
pub fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> {
|
pub fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> {
|
||||||
match format {
|
match format {
|
||||||
SerializationFormat::Text => {
|
SerializationFormat::Text | SerializationFormat::Grouped => {
|
||||||
println!(
|
println!(
|
||||||
"{} ({}): {}",
|
"{} ({}): {}",
|
||||||
code.as_ref(),
|
code.as_ref(),
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fmt;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use annotate_snippets::display_list::{DisplayList, FormatOptions};
|
|
||||||
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
|
|
||||||
use colored::Colorize;
|
|
||||||
use rustpython_parser::ast::Location;
|
use rustpython_parser::ast::Location;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::checks::{Check, CheckKind};
|
use crate::checks::{Check, CheckKind};
|
||||||
use crate::fs::relativize_path;
|
|
||||||
use crate::source_code_locator::SourceCodeLocator;
|
use crate::source_code_locator::SourceCodeLocator;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
@ -50,57 +44,6 @@ impl PartialOrd for Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Message {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let label = format!(
|
|
||||||
"{}{}{}{}{}{} {} {}",
|
|
||||||
relativize_path(Path::new(&self.filename)).bold(),
|
|
||||||
":".cyan(),
|
|
||||||
self.location.row(),
|
|
||||||
":".cyan(),
|
|
||||||
self.location.column(),
|
|
||||||
":".cyan(),
|
|
||||||
self.kind.code().as_ref().red().bold(),
|
|
||||||
self.kind.body(),
|
|
||||||
);
|
|
||||||
match &self.source {
|
|
||||||
None => write!(f, "{label}"),
|
|
||||||
Some(source) => {
|
|
||||||
let snippet = Snippet {
|
|
||||||
title: Some(Annotation {
|
|
||||||
label: Some(&label),
|
|
||||||
annotation_type: AnnotationType::Error,
|
|
||||||
// The ID (error number) is already encoded in the `label`.
|
|
||||||
id: None,
|
|
||||||
}),
|
|
||||||
footer: vec![],
|
|
||||||
slices: vec![Slice {
|
|
||||||
source: &source.contents,
|
|
||||||
line_start: self.location.row(),
|
|
||||||
annotations: vec![SourceAnnotation {
|
|
||||||
label: self.kind.code().as_ref(),
|
|
||||||
annotation_type: AnnotationType::Error,
|
|
||||||
range: source.range,
|
|
||||||
}],
|
|
||||||
// The origin (file name, line number, and column number) is already encoded
|
|
||||||
// in the `label`.
|
|
||||||
origin: None,
|
|
||||||
fold: false,
|
|
||||||
}],
|
|
||||||
opt: FormatOptions {
|
|
||||||
color: true,
|
|
||||||
..FormatOptions::default()
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// `split_once(' ')` strips "error: " from `message`.
|
|
||||||
let message = DisplayList::from(snippet).to_string();
|
|
||||||
let (_, message) = message.split_once(' ').unwrap();
|
|
||||||
write!(f, "{message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct Source {
|
pub struct Source {
|
||||||
pub contents: String,
|
pub contents: String,
|
||||||
|
|
198
src/printer.rs
198
src/printer.rs
|
@ -1,18 +1,27 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use annotate_snippets::display_list::{DisplayList, FormatOptions};
|
||||||
|
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
use itertools::iterate;
|
||||||
use rustpython_parser::ast::Location;
|
use rustpython_parser::ast::Location;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::checks::{CheckCode, CheckKind};
|
use crate::checks::{CheckCode, CheckKind};
|
||||||
|
use crate::fs::relativize_path;
|
||||||
use crate::linter::Diagnostics;
|
use crate::linter::Diagnostics;
|
||||||
use crate::logging::LogLevel;
|
use crate::logging::LogLevel;
|
||||||
|
use crate::message::Message;
|
||||||
use crate::tell_user;
|
use crate::tell_user;
|
||||||
|
|
||||||
#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)]
|
||||||
pub enum SerializationFormat {
|
pub enum SerializationFormat {
|
||||||
Text,
|
Text,
|
||||||
Json,
|
Json,
|
||||||
|
Grouped,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -41,6 +50,28 @@ impl<'a> Printer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pre_text(&self, diagnostics: &Diagnostics) {
|
||||||
|
if self.log_level >= &LogLevel::Default {
|
||||||
|
if diagnostics.fixed > 0 {
|
||||||
|
println!(
|
||||||
|
"Found {} error(s) ({} fixed).",
|
||||||
|
diagnostics.messages.len(),
|
||||||
|
diagnostics.fixed,
|
||||||
|
);
|
||||||
|
} else if !diagnostics.messages.is_empty() {
|
||||||
|
println!("Found {} error(s).", diagnostics.messages.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_text(&self, num_fixable: usize) {
|
||||||
|
if self.log_level >= &LogLevel::Default {
|
||||||
|
if num_fixable > 0 {
|
||||||
|
println!("{num_fixable} potentially fixable with the --fix option.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_once(&self, diagnostics: &Diagnostics) -> Result<()> {
|
pub fn write_once(&self, diagnostics: &Diagnostics) -> Result<()> {
|
||||||
if matches!(self.log_level, LogLevel::Silent) {
|
if matches!(self.log_level, LogLevel::Silent) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -73,27 +104,56 @@ impl<'a> Printer<'a> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
SerializationFormat::Text => {
|
SerializationFormat::Text => {
|
||||||
if self.log_level >= &LogLevel::Default {
|
self.pre_text(diagnostics);
|
||||||
if diagnostics.fixed > 0 {
|
|
||||||
println!(
|
|
||||||
"Found {} error(s) ({} fixed).",
|
|
||||||
diagnostics.messages.len(),
|
|
||||||
diagnostics.fixed,
|
|
||||||
);
|
|
||||||
} else if !diagnostics.messages.is_empty() {
|
|
||||||
println!("Found {} error(s).", diagnostics.messages.len());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for message in &diagnostics.messages {
|
for message in &diagnostics.messages {
|
||||||
println!("{message}");
|
print_message(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.log_level >= &LogLevel::Default {
|
self.post_text(num_fixable);
|
||||||
if num_fixable > 0 {
|
}
|
||||||
println!("{num_fixable} potentially fixable with the --fix option.");
|
SerializationFormat::Grouped => {
|
||||||
}
|
self.pre_text(diagnostics);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Group by filename.
|
||||||
|
let mut grouped_messages = BTreeMap::default();
|
||||||
|
for message in &diagnostics.messages {
|
||||||
|
grouped_messages
|
||||||
|
.entry(&message.filename)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (filename, messages) in grouped_messages {
|
||||||
|
// Compute the maximum number of digits in the row and column, for messages in
|
||||||
|
// this file.
|
||||||
|
let row_length = num_digits(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.map(|message| message.location.row())
|
||||||
|
.max()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let column_length = num_digits(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.map(|message| message.location.column())
|
||||||
|
.max()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Print the filename.
|
||||||
|
println!("{}:", relativize_path(Path::new(&filename)).underline());
|
||||||
|
|
||||||
|
// Print each message.
|
||||||
|
for message in messages {
|
||||||
|
print_grouped_message(message, row_length, column_length);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.post_text(num_fixable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +177,7 @@ impl<'a> Printer<'a> {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
for message in &diagnostics.messages {
|
for message in &diagnostics.messages {
|
||||||
println!("{message}");
|
print_message(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,3 +190,107 @@ impl<'a> Printer<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn num_digits(n: usize) -> usize {
|
||||||
|
iterate(n, |&n| n / 10)
|
||||||
|
.take_while(|&n| n > 0)
|
||||||
|
.count()
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a single `Message` with full details.
|
||||||
|
fn print_message(message: &Message) {
|
||||||
|
let label = format!(
|
||||||
|
"{}{}{}{}{}{} {} {}",
|
||||||
|
relativize_path(Path::new(&message.filename)).bold(),
|
||||||
|
":".cyan(),
|
||||||
|
message.location.row(),
|
||||||
|
":".cyan(),
|
||||||
|
message.location.column(),
|
||||||
|
":".cyan(),
|
||||||
|
message.kind.code().as_ref().red().bold(),
|
||||||
|
message.kind.body(),
|
||||||
|
);
|
||||||
|
println!("{label}");
|
||||||
|
if let Some(source) = &message.source {
|
||||||
|
let snippet = Snippet {
|
||||||
|
title: Some(Annotation {
|
||||||
|
label: None,
|
||||||
|
annotation_type: AnnotationType::Error,
|
||||||
|
// The ID (error number) is already encoded in the `label`.
|
||||||
|
id: None,
|
||||||
|
}),
|
||||||
|
footer: vec![],
|
||||||
|
slices: vec![Slice {
|
||||||
|
source: &source.contents,
|
||||||
|
line_start: message.location.row(),
|
||||||
|
annotations: vec![SourceAnnotation {
|
||||||
|
label: message.kind.code().as_ref(),
|
||||||
|
annotation_type: AnnotationType::Error,
|
||||||
|
range: source.range,
|
||||||
|
}],
|
||||||
|
// The origin (file name, line number, and column number) is already encoded
|
||||||
|
// in the `label`.
|
||||||
|
origin: None,
|
||||||
|
fold: false,
|
||||||
|
}],
|
||||||
|
opt: FormatOptions {
|
||||||
|
color: true,
|
||||||
|
..FormatOptions::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Skip the first line, since we format the `label` ourselves.
|
||||||
|
let message = DisplayList::from(snippet).to_string();
|
||||||
|
let (_, message) = message.split_once('\n').unwrap();
|
||||||
|
println!("{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a grouped `Message`, assumed to be printed in a group with others from
|
||||||
|
/// the same file.
|
||||||
|
fn print_grouped_message(message: &Message, row_length: usize, column_length: usize) {
|
||||||
|
let label = format!(
|
||||||
|
" {}{}{}{}{} {} {}",
|
||||||
|
" ".repeat(row_length - num_digits(message.location.row())),
|
||||||
|
message.location.row(),
|
||||||
|
":".cyan(),
|
||||||
|
message.location.column(),
|
||||||
|
" ".repeat(column_length - num_digits(message.location.column())),
|
||||||
|
message.kind.code().as_ref().red().bold(),
|
||||||
|
message.kind.body(),
|
||||||
|
);
|
||||||
|
println!("{label}");
|
||||||
|
if let Some(source) = &message.source {
|
||||||
|
let snippet = Snippet {
|
||||||
|
title: Some(Annotation {
|
||||||
|
label: None,
|
||||||
|
annotation_type: AnnotationType::Error,
|
||||||
|
// The ID (error number) is already encoded in the `label`.
|
||||||
|
id: None,
|
||||||
|
}),
|
||||||
|
footer: vec![],
|
||||||
|
slices: vec![Slice {
|
||||||
|
source: &source.contents,
|
||||||
|
line_start: message.location.row(),
|
||||||
|
annotations: vec![SourceAnnotation {
|
||||||
|
label: message.kind.code().as_ref(),
|
||||||
|
annotation_type: AnnotationType::Error,
|
||||||
|
range: source.range,
|
||||||
|
}],
|
||||||
|
// The origin (file name, line number, and column number) is already encoded
|
||||||
|
// in the `label`.
|
||||||
|
origin: None,
|
||||||
|
fold: false,
|
||||||
|
}],
|
||||||
|
opt: FormatOptions {
|
||||||
|
color: true,
|
||||||
|
..FormatOptions::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Skip the first line, since we format the `label` ourselves.
|
||||||
|
let message = DisplayList::from(snippet).to_string();
|
||||||
|
let (_, message) = message.split_once('\n').unwrap();
|
||||||
|
let message = textwrap::indent(message, " ");
|
||||||
|
println!("{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue