mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 14:52:01 +00:00
Extract message emitters from Printer (#3895)
This commit is contained in:
parent
333f1bd9ce
commit
9209e57c5a
29 changed files with 1470 additions and 592 deletions
|
@ -1,28 +1,23 @@
|
|||
use std::cmp::Reverse;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::hash::Hash;
|
||||
use std::io;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
use std::{env, io};
|
||||
|
||||
use annotate_snippets::display_list::{DisplayList, FormatOptions};
|
||||
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
|
||||
use anyhow::Result;
|
||||
use bitflags::bitflags;
|
||||
use colored::control::SHOULD_COLORIZE;
|
||||
use colored::Colorize;
|
||||
use itertools::{iterate, Itertools};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
use ruff::fs::{relativize_path, relativize_path_to};
|
||||
use ruff::jupyter::JupyterIndex;
|
||||
use ruff::fs::relativize_path;
|
||||
use ruff::linter::FixTable;
|
||||
use ruff::logging::LogLevel;
|
||||
use ruff::message::{Location, Message};
|
||||
use ruff::message::{
|
||||
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
|
||||
JsonEmitter, JunitEmitter, PylintEmitter, TextEmitter,
|
||||
};
|
||||
use ruff::notify_user;
|
||||
use ruff::registry::{AsRule, Rule};
|
||||
use ruff::settings::flags;
|
||||
|
@ -32,36 +27,12 @@ use crate::diagnostics::Diagnostics;
|
|||
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
pub struct Flags: u32 {
|
||||
pub(crate) struct Flags: u8 {
|
||||
const SHOW_VIOLATIONS = 0b0000_0001;
|
||||
const SHOW_FIXES = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExpandedEdit<'a> {
|
||||
content: &'a str,
|
||||
location: &'a Location,
|
||||
end_location: &'a Location,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExpandedFix<'a> {
|
||||
message: Option<&'a str>,
|
||||
edits: Vec<ExpandedEdit<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExpandedMessage<'a> {
|
||||
code: SerializeRuleAsCode,
|
||||
message: String,
|
||||
fix: Option<ExpandedFix<'a>>,
|
||||
location: Location,
|
||||
end_location: Location,
|
||||
filename: &'a str,
|
||||
noqa_row: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExpandedStatistics<'a> {
|
||||
code: SerializeRuleAsCode,
|
||||
|
@ -93,7 +64,7 @@ impl From<Rule> for SerializeRuleAsCode {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Printer {
|
||||
pub(crate) struct Printer {
|
||||
format: SerializationFormat,
|
||||
log_level: LogLevel,
|
||||
autofix_level: flags::FixMode,
|
||||
|
@ -121,7 +92,7 @@ impl Printer {
|
|||
}
|
||||
}
|
||||
|
||||
fn post_text<T: Write>(&self, stdout: &mut T, diagnostics: &Diagnostics) -> Result<()> {
|
||||
fn write_summary_text(&self, stdout: &mut dyn Write, diagnostics: &Diagnostics) -> Result<()> {
|
||||
if self.log_level >= LogLevel::Default {
|
||||
if self.flags.contains(Flags::SHOW_VIOLATIONS) {
|
||||
let fixed = diagnostics
|
||||
|
@ -192,103 +163,25 @@ impl Printer {
|
|||
writeln!(writer)?;
|
||||
}
|
||||
}
|
||||
self.post_text(writer, diagnostics)?;
|
||||
self.write_summary_text(writer, diagnostics)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = EmitterContext::new(&diagnostics.jupyter_index);
|
||||
|
||||
match self.format {
|
||||
SerializationFormat::Json => {
|
||||
writeln!(
|
||||
writer,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(
|
||||
&diagnostics
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| ExpandedMessage {
|
||||
code: message.kind.rule().into(),
|
||||
message: message.kind.body.clone(),
|
||||
fix: if message.fix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ExpandedFix {
|
||||
message: message.kind.suggestion.as_deref(),
|
||||
edits: message
|
||||
.fix
|
||||
.edits()
|
||||
.iter()
|
||||
.map(|edit| ExpandedEdit {
|
||||
content: &edit.content,
|
||||
location: &edit.location,
|
||||
end_location: &edit.end_location,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
},
|
||||
location: message.location,
|
||||
end_location: message.end_location,
|
||||
filename: &message.filename,
|
||||
noqa_row: message.noqa_row,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
)?
|
||||
)?;
|
||||
JsonEmitter::default().emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Junit => {
|
||||
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
|
||||
|
||||
let mut report = Report::new("ruff");
|
||||
for (filename, messages) in group_messages_by_filename(&diagnostics.messages) {
|
||||
let mut test_suite = TestSuite::new(filename);
|
||||
test_suite
|
||||
.extra
|
||||
.insert("package".to_string(), "org.ruff".to_string());
|
||||
for message in messages {
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.kind.body.clone());
|
||||
let description =
|
||||
if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
format!("line 1, col 0, {}", message.kind.body)
|
||||
} else {
|
||||
format!(
|
||||
"line {}, col {}, {}",
|
||||
message.location.row(),
|
||||
message.location.column(),
|
||||
message.kind.body
|
||||
)
|
||||
};
|
||||
status.set_description(description);
|
||||
let mut case = TestCase::new(
|
||||
format!("org.ruff.{}", message.kind.rule().noqa_code()),
|
||||
status,
|
||||
);
|
||||
let file_path = Path::new(filename);
|
||||
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
|
||||
let classname = file_path.parent().unwrap().join(file_stem);
|
||||
case.set_classname(classname.to_str().unwrap());
|
||||
case.extra
|
||||
.insert("line".to_string(), message.location.row().to_string());
|
||||
case.extra
|
||||
.insert("column".to_string(), message.location.column().to_string());
|
||||
|
||||
test_suite.add_test_case(case);
|
||||
}
|
||||
report.add_test_suite(test_suite);
|
||||
}
|
||||
writeln!(writer, "{}", report.to_string().unwrap())?;
|
||||
JunitEmitter::default().emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Text => {
|
||||
for message in &diagnostics.messages {
|
||||
print_message(
|
||||
writer,
|
||||
message,
|
||||
self.autofix_level,
|
||||
&diagnostics.jupyter_index,
|
||||
)?;
|
||||
}
|
||||
TextEmitter::default()
|
||||
.with_show_fix_status(show_fix_status(self.autofix_level))
|
||||
.emit(writer, &diagnostics.messages, &context)?;
|
||||
|
||||
if self.flags.contains(Flags::SHOW_FIXES) {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
writeln!(writer)?;
|
||||
|
@ -296,43 +189,14 @@ impl Printer {
|
|||
writeln!(writer)?;
|
||||
}
|
||||
}
|
||||
self.post_text(writer, diagnostics)?;
|
||||
|
||||
self.write_summary_text(writer, diagnostics)?;
|
||||
}
|
||||
SerializationFormat::Grouped => {
|
||||
for (filename, messages) in group_messages_by_filename(&diagnostics.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(),
|
||||
);
|
||||
GroupedEmitter::default()
|
||||
.with_show_fix_status(show_fix_status(self.autofix_level))
|
||||
.emit(writer, &diagnostics.messages, &context)?;
|
||||
|
||||
// Print the filename.
|
||||
writeln!(writer, "{}:", relativize_path(filename).underline())?;
|
||||
|
||||
// Print each message.
|
||||
for message in messages {
|
||||
print_grouped_message(
|
||||
writer,
|
||||
message,
|
||||
self.autofix_level,
|
||||
row_length,
|
||||
column_length,
|
||||
diagnostics.jupyter_index.get(filename),
|
||||
)?;
|
||||
}
|
||||
writeln!(writer)?;
|
||||
}
|
||||
if self.flags.contains(Flags::SHOW_FIXES) {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
writeln!(writer)?;
|
||||
|
@ -340,135 +204,19 @@ impl Printer {
|
|||
writeln!(writer)?;
|
||||
}
|
||||
}
|
||||
self.post_text(writer, diagnostics)?;
|
||||
self.write_summary_text(writer, diagnostics)?;
|
||||
}
|
||||
SerializationFormat::Github => {
|
||||
// Generate error workflow command in GitHub Actions format.
|
||||
// See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
||||
for message in &diagnostics.messages {
|
||||
let row_column = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
"1:0".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
message.location.row(),
|
||||
":",
|
||||
message.location.column(),
|
||||
)
|
||||
};
|
||||
let label = format!(
|
||||
"{}{}{}{} {} {}",
|
||||
relativize_path(&message.filename),
|
||||
":",
|
||||
row_column,
|
||||
":",
|
||||
message.kind.rule().noqa_code(),
|
||||
message.kind.body,
|
||||
);
|
||||
writeln!(
|
||||
writer,
|
||||
"::error title=Ruff \
|
||||
({}),file={},line={},col={},endLine={},endColumn={}::{}",
|
||||
message.kind.rule().noqa_code(),
|
||||
message.filename,
|
||||
message.location.row(),
|
||||
message.location.column(),
|
||||
message.end_location.row(),
|
||||
message.end_location.column(),
|
||||
label,
|
||||
)?;
|
||||
}
|
||||
GithubEmitter::default().emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Gitlab => {
|
||||
// Generate JSON with violations in GitLab CI format
|
||||
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
let project_dir = env::var("CI_PROJECT_DIR").ok();
|
||||
writeln!(writer,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(
|
||||
&diagnostics
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| {
|
||||
let lines = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
json!({
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"begin": message.location.row(),
|
||||
"end": message.end_location.row()
|
||||
})
|
||||
};
|
||||
json!({
|
||||
"description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body),
|
||||
"severity": "major",
|
||||
"fingerprint": fingerprint(message),
|
||||
"location": {
|
||||
"path": project_dir.as_ref().map_or_else(
|
||||
|| relativize_path(&message.filename),
|
||||
|project_dir| relativize_path_to(&message.filename, project_dir),
|
||||
),
|
||||
"lines": lines
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
)?
|
||||
)?;
|
||||
GitlabEmitter::default().emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Pylint => {
|
||||
// Generate violations in Pylint format.
|
||||
// See: https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter
|
||||
for message in &diagnostics.messages {
|
||||
let row = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
1
|
||||
} else {
|
||||
message.location.row()
|
||||
};
|
||||
let label = format!(
|
||||
"{}:{}: [{}] {}",
|
||||
relativize_path(&message.filename),
|
||||
row,
|
||||
message.kind.rule().noqa_code(),
|
||||
message.kind.body,
|
||||
);
|
||||
writeln!(writer, "{label}")?;
|
||||
}
|
||||
PylintEmitter::default().emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
SerializationFormat::Azure => {
|
||||
// Generate error logging commands for Azure Pipelines format.
|
||||
// See https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning
|
||||
for message in &diagnostics.messages {
|
||||
let row_column = if diagnostics.jupyter_index.contains_key(&message.filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
"linenumber=1;columnnumber=0".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"linenumber={};columnnumber={}",
|
||||
message.location.row(),
|
||||
message.location.column(),
|
||||
)
|
||||
};
|
||||
writeln!(
|
||||
writer,
|
||||
"##vso[task.logissue type=error\
|
||||
;sourcepath={};{};code={};]{}",
|
||||
message.filename,
|
||||
row_column,
|
||||
message.kind.rule().noqa_code(),
|
||||
message.kind.body,
|
||||
)?;
|
||||
}
|
||||
AzureEmitter::default().emit(writer, &diagnostics.messages, &context)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -594,14 +342,11 @@ impl Printer {
|
|||
if self.log_level >= LogLevel::Default {
|
||||
writeln!(stdout)?;
|
||||
}
|
||||
for message in &diagnostics.messages {
|
||||
print_message(
|
||||
&mut stdout,
|
||||
message,
|
||||
self.autofix_level,
|
||||
&diagnostics.jupyter_index,
|
||||
)?;
|
||||
}
|
||||
|
||||
let context = EmitterContext::new(&diagnostics.jupyter_index);
|
||||
TextEmitter::default()
|
||||
.with_show_fix_status(show_fix_status(self.autofix_level))
|
||||
.emit(&mut stdout, &diagnostics.messages, &context)?;
|
||||
}
|
||||
stdout.flush()?;
|
||||
|
||||
|
@ -615,17 +360,6 @@ impl Printer {
|
|||
}
|
||||
}
|
||||
|
||||
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<&Message>> {
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for message in messages {
|
||||
grouped_messages
|
||||
.entry(message.filename.as_str())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(message);
|
||||
}
|
||||
grouped_messages
|
||||
}
|
||||
|
||||
fn num_digits(n: usize) -> usize {
|
||||
iterate(n, |&n| n / 10)
|
||||
.take_while(|&n| n > 0)
|
||||
|
@ -633,43 +367,8 @@ fn num_digits(n: usize) -> usize {
|
|||
.max(1)
|
||||
}
|
||||
|
||||
/// Generate a unique fingerprint to identify a violation.
|
||||
fn fingerprint(message: &Message) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
message.kind.rule().hash(&mut hasher);
|
||||
message.location.row().hash(&mut hasher);
|
||||
message.location.column().hash(&mut hasher);
|
||||
message.end_location.row().hash(&mut hasher);
|
||||
message.end_location.column().hash(&mut hasher);
|
||||
message.filename.hash(&mut hasher);
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
|
||||
struct CodeAndBody<'a>(&'a Message, flags::FixMode);
|
||||
|
||||
impl Display for CodeAndBody<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if show_fix_status(self.1) && self.0.kind.fixable {
|
||||
write!(
|
||||
f,
|
||||
"{code} {autofix}{body}",
|
||||
code = self.0.kind.rule().noqa_code().to_string().red().bold(),
|
||||
autofix = format_args!("[{}] ", "*".cyan()),
|
||||
body = self.0.kind.body,
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{code} {body}",
|
||||
code = self.0.kind.rule().noqa_code().to_string().red().bold(),
|
||||
body = self.0.kind.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Printer`] should indicate that a rule is fixable.
|
||||
fn show_fix_status(autofix_level: flags::FixMode) -> bool {
|
||||
const fn show_fix_status(autofix_level: flags::FixMode) -> bool {
|
||||
// If we're in application mode, avoid indicating that a rule is fixable.
|
||||
// If the specific violation were truly fixable, it would've been fixed in
|
||||
// this pass! (We're occasionally unable to determine whether a specific
|
||||
|
@ -678,84 +377,6 @@ fn show_fix_status(autofix_level: flags::FixMode) -> bool {
|
|||
!matches!(autofix_level, flags::FixMode::Apply)
|
||||
}
|
||||
|
||||
/// Print a single `Message` with full details.
|
||||
fn print_message<T: Write>(
|
||||
stdout: &mut T,
|
||||
message: &Message,
|
||||
autofix_level: flags::FixMode,
|
||||
jupyter_index: &FxHashMap<String, JupyterIndex>,
|
||||
) -> Result<()> {
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let pos_label = if let Some(jupyter_index) = jupyter_index.get(&message.filename) {
|
||||
format!(
|
||||
"cell {cell}{sep}{row}{sep}{col}{sep}",
|
||||
cell = jupyter_index.row_to_cell[message.location.row()],
|
||||
sep = ":".cyan(),
|
||||
row = jupyter_index.row_to_row_in_cell[message.location.row()],
|
||||
col = message.location.column(),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{row}{sep}{col}{sep}",
|
||||
sep = ":".cyan(),
|
||||
row = message.location.row(),
|
||||
col = message.location.column(),
|
||||
)
|
||||
};
|
||||
writeln!(
|
||||
stdout,
|
||||
"{path}{sep}{pos_label} {code_and_body}",
|
||||
path = relativize_path(&message.filename).bold(),
|
||||
sep = ":".cyan(),
|
||||
pos_label = pos_label,
|
||||
code_and_body = CodeAndBody(message, autofix_level)
|
||||
)?;
|
||||
if let Some(source) = &message.source {
|
||||
let suggestion = message.kind.suggestion.clone();
|
||||
let footer = if suggestion.is_some() {
|
||||
vec![Annotation {
|
||||
id: None,
|
||||
label: suggestion.as_deref(),
|
||||
annotation_type: AnnotationType::Help,
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let label = message.kind.rule().noqa_code().to_string();
|
||||
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,
|
||||
slices: vec![Slice {
|
||||
source: &source.contents,
|
||||
line_start: message.location.row(),
|
||||
annotations: vec![SourceAnnotation {
|
||||
label: &label,
|
||||
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: SHOULD_COLORIZE.should_colorize(),
|
||||
..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();
|
||||
writeln!(stdout, "{message}\n")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_fixed<T: Write>(stdout: &mut T, fixed: &FxHashMap<String, FixTable>) -> Result<()> {
|
||||
let total = fixed
|
||||
.values()
|
||||
|
@ -796,85 +417,3 @@ fn print_fixed<T: Write>(stdout: &mut T, fixed: &FxHashMap<String, FixTable>) ->
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print a grouped `Message`, assumed to be printed in a group with others from
|
||||
/// the same file.
|
||||
fn print_grouped_message<T: Write>(
|
||||
stdout: &mut T,
|
||||
message: &Message,
|
||||
autofix_level: flags::FixMode,
|
||||
row_length: usize,
|
||||
column_length: usize,
|
||||
jupyter_index: Option<&JupyterIndex>,
|
||||
) -> Result<()> {
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let pos_label = if let Some(jupyter_index) = jupyter_index {
|
||||
format!(
|
||||
"cell {cell}{sep}{row}{sep}{col}",
|
||||
cell = jupyter_index.row_to_cell[message.location.row()],
|
||||
sep = ":".cyan(),
|
||||
row = jupyter_index.row_to_row_in_cell[message.location.row()],
|
||||
col = message.location.column(),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{row}{sep}{col}",
|
||||
sep = ":".cyan(),
|
||||
row = message.location.row(),
|
||||
col = message.location.column(),
|
||||
)
|
||||
};
|
||||
let label = format!(
|
||||
" {row_padding}{pos_label}{col_padding} {code_and_body}",
|
||||
row_padding = " ".repeat(row_length - num_digits(message.location.row())),
|
||||
pos_label = pos_label,
|
||||
col_padding = " ".repeat(column_length - num_digits(message.location.column())),
|
||||
code_and_body = CodeAndBody(message, autofix_level),
|
||||
);
|
||||
writeln!(stdout, "{label}")?;
|
||||
if let Some(source) = &message.source {
|
||||
let suggestion = message.kind.suggestion.clone();
|
||||
let footer = if suggestion.is_some() {
|
||||
vec![Annotation {
|
||||
id: None,
|
||||
label: suggestion.as_deref(),
|
||||
annotation_type: AnnotationType::Help,
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let label = message.kind.rule().noqa_code().to_string();
|
||||
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,
|
||||
slices: vec![Slice {
|
||||
source: &source.contents,
|
||||
line_start: message.location.row(),
|
||||
annotations: vec![SourceAnnotation {
|
||||
label: &label,
|
||||
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: SHOULD_COLORIZE.should_colorize(),
|
||||
..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, " ");
|
||||
writeln!(stdout, "{message}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue