Grouped format implementation (#954)

This commit is contained in:
Hayden 2022-11-29 14:45:16 -09:00 committed by GitHub
parent 602291c0c2
commit ca38c7ac48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 187 additions and 78 deletions

View file

@ -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

View file

@ -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(),

View file

@ -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,

View file

@ -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}");
}
}