Remove TextEmitter (#20595)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

Addresses
https://github.com/astral-sh/ruff/pull/20443#discussion_r2381237640 by
factoring out the `match` on the ruff output format in a way that should
be reusable by the formatter.

I didn't think this was going to work at first, but the fact that the
config holds options that apply only to certain output formats works in
our favor here. We can set up a single config for all of the output
formats and then use `try_from` to convert the `OutputFormat` to a
`DiagnosticFormat` later.

## Test Plan

Existing tests, plus a few new ones to make sure relocating the
`SHOW_FIX_SUMMARY` rendering worked, that was untested before. I deleted
a bunch of test code along with the `text` module, but I believe all of
it is now well-covered by the `full` and `concise` tests in `ruff_db`.

I also merged this branch into
https://github.com/astral-sh/ruff/pull/20443 locally and made sure that
the API actually helps. `render_diagnostics` dropped in perfectly and
passed the tests there too.
This commit is contained in:
Brent Westbrook 2025-09-29 08:46:25 -04:00 committed by GitHub
parent 1cf19732b9
commit 00c8851ef8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 285 additions and 548 deletions

View file

@ -227,7 +227,8 @@ mod test {
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use tempfile::TempDir; use tempfile::TempDir;
use ruff_linter::message::{Emitter, EmitterContext, TextEmitter}; use ruff_db::diagnostic::{DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics};
use ruff_linter::message::EmitterContext;
use ruff_linter::registry::Rule; use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes; use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{LinterSettings, flags}; use ruff_linter::settings::{LinterSettings, flags};
@ -280,19 +281,16 @@ mod test {
UnsafeFixes::Enabled, UnsafeFixes::Enabled,
) )
.unwrap(); .unwrap();
let mut output = Vec::new();
TextEmitter::default() let config = DisplayDiagnosticConfig::default()
.with_show_fix_status(true) .format(DiagnosticFormat::Concise)
.with_color(false) .hide_severity(true);
.emit( let messages = DisplayDiagnostics::new(
&mut output, &EmitterContext::new(&FxHashMap::default()),
&diagnostics.inner, &config,
&EmitterContext::new(&FxHashMap::default()), &diagnostics.inner,
) )
.unwrap(); .to_string();
let messages = String::from_utf8(output).unwrap();
insta::with_settings!({ insta::with_settings!({
omit_expression => true, omit_expression => true,

View file

@ -10,12 +10,11 @@ use ruff_linter::linter::FixTable;
use serde::Serialize; use serde::Serialize;
use ruff_db::diagnostic::{ use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
DisplayGithubDiagnostics, GithubRenderer, SecondaryCode,
}; };
use ruff_linter::fs::relativize_path; use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel; use ruff_linter::logging::LogLevel;
use ruff_linter::message::{Emitter, EmitterContext, GroupedEmitter, SarifEmitter, TextEmitter}; use ruff_linter::message::{EmitterContext, render_diagnostics};
use ruff_linter::notify_user; use ruff_linter::notify_user;
use ruff_linter::settings::flags::{self}; use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{OutputFormat, UnsafeFixes}; use ruff_linter::settings::types::{OutputFormat, UnsafeFixes};
@ -225,86 +224,28 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes); let context = EmitterContext::new(&diagnostics.notebook_indexes);
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes); let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
let config = DisplayDiagnosticConfig::default().preview(preview); let config = DisplayDiagnosticConfig::default()
.preview(preview)
.hide_severity(true)
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_fix_applicability(self.unsafe_fixes.required_applicability())
.show_fix_diff(preview);
match self.format { render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?;
OutputFormat::Json => {
let config = config.format(DiagnosticFormat::Json);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Rdjson => {
let config = config.format(DiagnosticFormat::Rdjson);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::JsonLines => {
let config = config.format(DiagnosticFormat::JsonLines);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Junit => {
let config = config.format(DiagnosticFormat::Junit);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Concise | OutputFormat::Full => {
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_fix_diff(self.format == OutputFormat::Full && preview)
.with_show_source(self.format == OutputFormat::Full)
.with_fix_applicability(self.unsafe_fixes.required_applicability())
.with_preview(preview)
.emit(writer, &diagnostics.inner, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if matches!(
if !diagnostics.fixed.is_empty() { self.format,
writeln!(writer)?; OutputFormat::Full | OutputFormat::Concise | OutputFormat::Grouped
print_fix_summary(writer, &diagnostics.fixed)?; ) {
writeln!(writer)?; if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
} if !diagnostics.fixed.is_empty() {
writeln!(writer)?;
print_fix_summary(writer, &diagnostics.fixed)?;
writeln!(writer)?;
} }
self.write_summary_text(writer, diagnostics)?;
}
OutputFormat::Grouped => {
GroupedEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.inner, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
if !diagnostics.fixed.is_empty() {
writeln!(writer)?;
print_fix_summary(writer, &diagnostics.fixed)?;
writeln!(writer)?;
}
}
self.write_summary_text(writer, diagnostics)?;
}
OutputFormat::Github => {
let renderer = GithubRenderer::new(&context, "Ruff");
let value = DisplayGithubDiagnostics::new(&renderer, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Gitlab => {
let config = config.format(DiagnosticFormat::Gitlab);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Pylint => {
let config = config.format(DiagnosticFormat::Pylint);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Azure => {
let config = config.format(DiagnosticFormat::Azure);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Sarif => {
SarifEmitter.emit(writer, &diagnostics.inner, &context)?;
} }
self.write_summary_text(writer, diagnostics)?;
} }
writer.flush()?; writer.flush()?;
@ -448,11 +389,22 @@ impl Printer {
} }
let context = EmitterContext::new(&diagnostics.notebook_indexes); let context = EmitterContext::new(&diagnostics.notebook_indexes);
TextEmitter::default() let format = if preview {
DiagnosticFormat::Full
} else {
DiagnosticFormat::Concise
};
let config = DisplayDiagnosticConfig::default()
.hide_severity(true)
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_source(preview) .format(format)
.with_fix_applicability(self.unsafe_fixes.required_applicability()) .with_fix_applicability(self.unsafe_fixes.required_applicability());
.emit(writer, &diagnostics.inner, &context)?; write!(
writer,
"{}",
DisplayDiagnostics::new(&context, &config, &diagnostics.inner)
)?;
} }
writer.flush()?; writer.flush()?;

View file

@ -6199,6 +6199,36 @@ match 42: # invalid-syntax
Ok(()) Ok(())
} }
#[test_case::test_case("concise"; "concise_show_fixes")]
#[test_case::test_case("full"; "full_show_fixes")]
#[test_case::test_case("grouped"; "grouped_show_fixes")]
fn output_format_show_fixes(output_format: &str) -> Result<()> {
let tempdir = TempDir::new()?;
let input = tempdir.path().join("input.py");
fs::write(&input, "import os # F401")?;
let snapshot = format!("output_format_show_fixes_{output_format}");
assert_cmd_snapshot!(
snapshot,
Command::new(get_cargo_bin(BIN_NAME))
.args([
"check",
"--no-cache",
"--output-format",
output_format,
"--select",
"F401",
"--fix",
"--show-fixes",
"input.py",
])
.current_dir(&tempdir),
);
Ok(())
}
#[test] #[test]
fn up045_nested_optional_flatten_all() { fn up045_nested_optional_flatten_all() {
let contents = "\ let contents = "\

View file

@ -0,0 +1,26 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- concise
- "--select"
- F401
- "--fix"
- "--show-fixes"
- input.py
---
success: true
exit_code: 0
----- stdout -----
Fixed 1 error:
- input.py:
1 × F401 (unused-import)
Found 1 error (1 fixed, 0 remaining).
----- stderr -----

View file

@ -0,0 +1,26 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- full
- "--select"
- F401
- "--fix"
- "--show-fixes"
- input.py
---
success: true
exit_code: 0
----- stdout -----
Fixed 1 error:
- input.py:
1 × F401 (unused-import)
Found 1 error (1 fixed, 0 remaining).
----- stderr -----

View file

@ -0,0 +1,26 @@
---
source: crates/ruff/tests/lint.rs
info:
program: ruff
args:
- check
- "--no-cache"
- "--output-format"
- grouped
- "--select"
- F401
- "--fix"
- "--show-fixes"
- input.py
---
success: true
exit_code: 0
----- stdout -----
Fixed 1 error:
- input.py:
1 × F401 (unused-import)
Found 1 error (1 fixed, 0 remaining).
----- stderr -----

View file

@ -1353,7 +1353,7 @@ impl DisplayDiagnosticConfig {
} }
/// Whether to show a fix's availability or not. /// Whether to show a fix's availability or not.
pub fn show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig { pub fn with_show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig {
DisplayDiagnosticConfig { DisplayDiagnosticConfig {
show_fix_status: yes, show_fix_status: yes,
..self ..self
@ -1374,12 +1374,20 @@ impl DisplayDiagnosticConfig {
/// availability for unsafe or display-only fixes. /// availability for unsafe or display-only fixes.
/// ///
/// Note that this option is currently ignored when `hide_severity` is false. /// Note that this option is currently ignored when `hide_severity` is false.
pub fn fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig { pub fn with_fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig {
DisplayDiagnosticConfig { DisplayDiagnosticConfig {
fix_applicability: applicability, fix_applicability: applicability,
..self ..self
} }
} }
pub fn show_fix_status(&self) -> bool {
self.show_fix_status
}
pub fn fix_applicability(&self) -> Applicability {
self.fix_applicability
}
} }
impl Default for DisplayDiagnosticConfig { impl Default for DisplayDiagnosticConfig {

View file

@ -2618,7 +2618,7 @@ watermelon
/// Show fix availability when rendering. /// Show fix availability when rendering.
pub(super) fn show_fix_status(&mut self, yes: bool) { pub(super) fn show_fix_status(&mut self, yes: bool) {
let mut config = std::mem::take(&mut self.config); let mut config = std::mem::take(&mut self.config);
config = config.show_fix_status(yes); config = config.with_show_fix_status(yes);
self.config = config; self.config = config;
} }
@ -2632,7 +2632,7 @@ watermelon
/// The lowest fix applicability to show when rendering. /// The lowest fix applicability to show when rendering.
pub(super) fn fix_applicability(&mut self, applicability: Applicability) { pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
let mut config = std::mem::take(&mut self.config); let mut config = std::mem::take(&mut self.config);
config = config.fix_applicability(applicability); config = config.with_fix_applicability(applicability);
self.config = config; self.config = config;
} }

View file

@ -6,17 +6,25 @@ use std::num::NonZeroUsize;
use colored::Colorize; use colored::Colorize;
use ruff_db::diagnostic::Diagnostic; use ruff_db::diagnostic::Diagnostic;
use ruff_diagnostics::Applicability;
use ruff_notebook::NotebookIndex; use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, OneIndexed}; use ruff_source_file::{LineColumn, OneIndexed};
use crate::fs::relativize_path; use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext}; use crate::message::{Emitter, EmitterContext};
use crate::settings::types::UnsafeFixes;
#[derive(Default)]
pub struct GroupedEmitter { pub struct GroupedEmitter {
show_fix_status: bool, show_fix_status: bool,
unsafe_fixes: UnsafeFixes, applicability: Applicability,
}
impl Default for GroupedEmitter {
fn default() -> Self {
Self {
show_fix_status: false,
applicability: Applicability::Safe,
}
}
} }
impl GroupedEmitter { impl GroupedEmitter {
@ -27,8 +35,8 @@ impl GroupedEmitter {
} }
#[must_use] #[must_use]
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self { pub fn with_applicability(mut self, applicability: Applicability) -> Self {
self.unsafe_fixes = unsafe_fixes; self.applicability = applicability;
self self
} }
} }
@ -67,7 +75,7 @@ impl Emitter for GroupedEmitter {
notebook_index: context.notebook_index(&message.expect_ruff_filename()), notebook_index: context.notebook_index(&message.expect_ruff_filename()),
message, message,
show_fix_status: self.show_fix_status, show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes, applicability: self.applicability,
row_length, row_length,
column_length, column_length,
} }
@ -114,7 +122,7 @@ fn group_diagnostics_by_filename(
struct DisplayGroupedMessage<'a> { struct DisplayGroupedMessage<'a> {
message: MessageWithLocation<'a>, message: MessageWithLocation<'a>,
show_fix_status: bool, show_fix_status: bool,
unsafe_fixes: UnsafeFixes, applicability: Applicability,
row_length: NonZeroUsize, row_length: NonZeroUsize,
column_length: NonZeroUsize, column_length: NonZeroUsize,
notebook_index: Option<&'a NotebookIndex>, notebook_index: Option<&'a NotebookIndex>,
@ -162,7 +170,7 @@ impl Display for DisplayGroupedMessage<'_> {
code_and_body = RuleCodeAndBody { code_and_body = RuleCodeAndBody {
message, message,
show_fix_status: self.show_fix_status, show_fix_status: self.show_fix_status,
unsafe_fixes: self.unsafe_fixes applicability: self.applicability
}, },
)?; )?;
@ -173,7 +181,7 @@ impl Display for DisplayGroupedMessage<'_> {
pub(super) struct RuleCodeAndBody<'a> { pub(super) struct RuleCodeAndBody<'a> {
pub(crate) message: &'a Diagnostic, pub(crate) message: &'a Diagnostic,
pub(crate) show_fix_status: bool, pub(crate) show_fix_status: bool,
pub(crate) unsafe_fixes: UnsafeFixes, pub(crate) applicability: Applicability,
} }
impl Display for RuleCodeAndBody<'_> { impl Display for RuleCodeAndBody<'_> {
@ -181,7 +189,7 @@ impl Display for RuleCodeAndBody<'_> {
if self.show_fix_status { if self.show_fix_status {
if let Some(fix) = self.message.fix() { if let Some(fix) = self.message.fix() {
// Do not display an indicator for inapplicable fixes // Do not display an indicator for inapplicable fixes
if fix.applies(self.unsafe_fixes.required_applicability()) { if fix.applies(self.applicability) {
if let Some(code) = self.message.secondary_code() { if let Some(code) = self.message.secondary_code() {
write!(f, "{} ", code.red().bold())?; write!(f, "{} ", code.red().bold())?;
} }
@ -217,11 +225,12 @@ impl Display for RuleCodeAndBody<'_> {
mod tests { mod tests {
use insta::assert_snapshot; use insta::assert_snapshot;
use ruff_diagnostics::Applicability;
use crate::message::GroupedEmitter; use crate::message::GroupedEmitter;
use crate::message::tests::{ use crate::message::tests::{
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
}; };
use crate::settings::types::UnsafeFixes;
#[test] #[test]
fn default() { fn default() {
@ -251,7 +260,7 @@ mod tests {
fn fix_status_unsafe() { fn fix_status_unsafe() {
let mut emitter = GroupedEmitter::default() let mut emitter = GroupedEmitter::default()
.with_show_fix_status(true) .with_show_fix_status(true)
.with_unsafe_fixes(UnsafeFixes::Enabled); .with_applicability(Applicability::Unsafe);
let content = capture_emitter_output(&mut emitter, &create_diagnostics()); let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content); assert_snapshot!(content);

View file

@ -4,8 +4,9 @@ use std::io::Write;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_db::diagnostic::{ use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, FileResolver, Input, LintName, SecondaryCode, Severity, Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
Span, UnifiedFile, DisplayDiagnostics, DisplayGithubDiagnostics, FileResolver, GithubRenderer, Input, LintName,
SecondaryCode, Severity, Span, UnifiedFile,
}; };
use ruff_db::files::File; use ruff_db::files::File;
@ -14,14 +15,13 @@ use ruff_notebook::NotebookIndex;
use ruff_source_file::SourceFile; use ruff_source_file::SourceFile;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
pub use sarif::SarifEmitter; pub use sarif::SarifEmitter;
pub use text::TextEmitter;
use crate::Fix; use crate::Fix;
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::types::{OutputFormat, RuffOutputFormat};
mod grouped; mod grouped;
mod sarif; mod sarif;
mod text;
/// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff. /// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff.
/// ///
@ -160,14 +160,48 @@ impl<'a> EmitterContext<'a> {
} }
} }
pub fn render_diagnostics(
writer: &mut dyn Write,
format: OutputFormat,
config: DisplayDiagnosticConfig,
context: &EmitterContext<'_>,
diagnostics: &[Diagnostic],
) -> std::io::Result<()> {
match DiagnosticFormat::try_from(format) {
Ok(format) => {
let config = config.format(format);
let value = DisplayDiagnostics::new(context, &config, diagnostics);
write!(writer, "{value}")?;
}
Err(RuffOutputFormat::Github) => {
let renderer = GithubRenderer::new(context, "Ruff");
let value = DisplayGithubDiagnostics::new(&renderer, diagnostics);
write!(writer, "{value}")?;
}
Err(RuffOutputFormat::Grouped) => {
GroupedEmitter::default()
.with_show_fix_status(config.show_fix_status())
.with_applicability(config.fix_applicability())
.emit(writer, diagnostics, context)
.map_err(std::io::Error::other)?;
}
Err(RuffOutputFormat::Sarif) => {
SarifEmitter
.emit(writer, diagnostics, context)
.map_err(std::io::Error::other)?;
}
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_db::diagnostic::Diagnostic; use ruff_db::diagnostic::Diagnostic;
use ruff_notebook::NotebookIndex;
use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; use ruff_python_parser::{Mode, ParseOptions, parse_unchecked};
use ruff_source_file::{OneIndexed, SourceFileBuilder}; use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use crate::codes::Rule; use crate::codes::Rule;
@ -257,104 +291,6 @@ def fibonacci(n):
vec![unused_import, unused_variable, undefined_name] vec![unused_import, unused_variable, undefined_name]
} }
pub(super) fn create_notebook_diagnostics()
-> (Vec<Diagnostic>, FxHashMap<String, NotebookIndex>) {
let notebook = r"# cell 1
import os
# cell 2
import math
print('hello world')
# cell 3
def foo():
print()
x = 1
";
let notebook_source = SourceFileBuilder::new("notebook.ipynb", notebook).finish();
let unused_import_os_start = TextSize::from(16);
let unused_import_os = create_lint_diagnostic(
"`os` imported but unused",
Some("Remove unused import: `os`"),
TextRange::new(unused_import_os_start, TextSize::from(18)),
Some(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(9),
TextSize::from(19),
)))),
None,
notebook_source.clone(),
Some(unused_import_os_start),
Rule::UnusedImport,
);
let unused_import_math_start = TextSize::from(35);
let unused_import_math = create_lint_diagnostic(
"`math` imported but unused",
Some("Remove unused import: `math`"),
TextRange::new(unused_import_math_start, TextSize::from(39)),
Some(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(28),
TextSize::from(40),
)))),
None,
notebook_source.clone(),
Some(unused_import_math_start),
Rule::UnusedImport,
);
let unused_variable_start = TextSize::from(98);
let unused_variable = create_lint_diagnostic(
"Local variable `x` is assigned to but never used",
Some("Remove assignment to unused variable `x`"),
TextRange::new(unused_variable_start, TextSize::from(99)),
Some(Fix::unsafe_edit(Edit::deletion(
TextSize::from(94),
TextSize::from(104),
))),
None,
notebook_source,
Some(unused_variable_start),
Rule::UnusedVariable,
);
let mut notebook_indexes = FxHashMap::default();
notebook_indexes.insert(
"notebook.ipynb".to_string(),
NotebookIndex::new(
vec![
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(2),
],
vec![
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(3),
OneIndexed::from_zero_indexed(0),
OneIndexed::from_zero_indexed(1),
OneIndexed::from_zero_indexed(2),
OneIndexed::from_zero_indexed(3),
],
),
);
(
vec![unused_import_os, unused_import_math, unused_variable],
notebook_indexes,
)
}
pub(super) fn capture_emitter_output( pub(super) fn capture_emitter_output(
emitter: &mut dyn Emitter, emitter: &mut dyn Emitter,
diagnostics: &[Diagnostic], diagnostics: &[Diagnostic],
@ -366,16 +302,4 @@ def foo():
String::from_utf8(output).expect("Output to be valid UTF-8") String::from_utf8(output).expect("Output to be valid UTF-8")
} }
pub(super) fn capture_emitter_notebook_output(
emitter: &mut dyn Emitter,
diagnostics: &[Diagnostic],
notebook_indexes: &FxHashMap<String, NotebookIndex>,
) -> String {
let context = EmitterContext::new(notebook_indexes);
let mut output: Vec<u8> = Vec::new();
emitter.emit(&mut output, diagnostics, &context).unwrap();
String::from_utf8(output).expect("Output to be valid UTF-8")
}
} }

View file

@ -1,30 +0,0 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
F401 `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
|
help: Remove unused import: `os`
F841 Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
F821 Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
|

View file

@ -1,30 +0,0 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
F401 `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
|
help: Remove unused import: `os`
F841 Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
F821 Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
|

View file

@ -1,30 +0,0 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
F401 [*] `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
|
help: Remove unused import: `os`
F841 [*] Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
F821 Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
|

View file

@ -1,33 +0,0 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
F401 [*] `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
F401 [*] `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
F841 [*] Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`

View file

@ -1,23 +0,0 @@
---
source: crates/ruff_linter/src/message/text.rs
expression: content
---
invalid-syntax: Expected one or more symbol names after import
--> syntax_errors.py:1:15
|
1 | from os import
| ^
2 |
3 | if call(foo
|
invalid-syntax: Expected ')', found newline
--> syntax_errors.py:3:12
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|

View file

@ -1,143 +0,0 @@
use std::io::Write;
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
};
use ruff_diagnostics::Applicability;
use crate::message::{Emitter, EmitterContext};
pub struct TextEmitter {
config: DisplayDiagnosticConfig,
}
impl Default for TextEmitter {
fn default() -> Self {
Self {
config: DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Concise)
.hide_severity(true)
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()),
}
}
}
impl TextEmitter {
#[must_use]
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
self.config = self.config.show_fix_status(show_fix_status);
self
}
#[must_use]
pub fn with_show_fix_diff(mut self, show_fix_diff: bool) -> Self {
self.config = self.config.show_fix_diff(show_fix_diff);
self
}
#[must_use]
pub fn with_show_source(mut self, show_source: bool) -> Self {
self.config = self.config.format(if show_source {
DiagnosticFormat::Full
} else {
DiagnosticFormat::Concise
});
self
}
#[must_use]
pub fn with_fix_applicability(mut self, applicability: Applicability) -> Self {
self.config = self.config.fix_applicability(applicability);
self
}
#[must_use]
pub fn with_preview(mut self, preview: bool) -> Self {
self.config = self.config.preview(preview);
self
}
#[must_use]
pub fn with_color(mut self, color: bool) -> Self {
self.config = self.config.color(color);
self
}
}
impl Emitter for TextEmitter {
fn emit(
&mut self,
writer: &mut dyn Write,
diagnostics: &[Diagnostic],
context: &EmitterContext,
) -> anyhow::Result<()> {
write!(
writer,
"{}",
DisplayDiagnostics::new(context, &self.config, diagnostics)
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use ruff_diagnostics::Applicability;
use crate::message::TextEmitter;
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_diagnostics,
create_notebook_diagnostics, create_syntax_error_diagnostics,
};
#[test]
fn default() {
let mut emitter = TextEmitter::default().with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content);
}
#[test]
fn fix_status() {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content);
}
#[test]
fn fix_status_unsafe() {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_fix_applicability(Applicability::Unsafe);
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content);
}
#[test]
fn notebook_output() {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_fix_applicability(Applicability::Unsafe);
let (messages, notebook_indexes) = create_notebook_diagnostics();
let content = capture_emitter_notebook_output(&mut emitter, &messages, &notebook_indexes);
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = TextEmitter::default().with_show_source(true);
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
assert_snapshot!(content);
}
}

View file

@ -9,6 +9,7 @@ use anyhow::{Context, Result, bail};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use log::debug; use log::debug;
use pep440_rs::{VersionSpecifier, VersionSpecifiers}; use pep440_rs::{VersionSpecifier, VersionSpecifiers};
use ruff_db::diagnostic::DiagnosticFormat;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Deserializer, Serialize, de}; use serde::{Deserialize, Deserializer, Serialize, de};
use strum_macros::EnumIter; use strum_macros::EnumIter;
@ -553,6 +554,34 @@ impl Display for OutputFormat {
} }
} }
/// The subset of output formats only implemented in Ruff, not in `ruff_db` via `DisplayDiagnostics`.
pub enum RuffOutputFormat {
Github,
Grouped,
Sarif,
}
impl TryFrom<OutputFormat> for DiagnosticFormat {
type Error = RuffOutputFormat;
fn try_from(format: OutputFormat) -> std::result::Result<Self, Self::Error> {
match format {
OutputFormat::Concise => Ok(DiagnosticFormat::Concise),
OutputFormat::Full => Ok(DiagnosticFormat::Full),
OutputFormat::Json => Ok(DiagnosticFormat::Json),
OutputFormat::JsonLines => Ok(DiagnosticFormat::JsonLines),
OutputFormat::Junit => Ok(DiagnosticFormat::Junit),
OutputFormat::Gitlab => Ok(DiagnosticFormat::Gitlab),
OutputFormat::Pylint => Ok(DiagnosticFormat::Pylint),
OutputFormat::Rdjson => Ok(DiagnosticFormat::Rdjson),
OutputFormat::Azure => Ok(DiagnosticFormat::Azure),
OutputFormat::Github => Err(RuffOutputFormat::Github),
OutputFormat::Grouped => Err(RuffOutputFormat::Grouped),
OutputFormat::Sarif => Err(RuffOutputFormat::Sarif),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(try_from = "String")] #[serde(try_from = "String")]
pub struct RequiredVersion(VersionSpecifiers); pub struct RequiredVersion(VersionSpecifiers);

View file

@ -10,7 +10,9 @@ use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_db::diagnostic::{Diagnostic, Span}; use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, Span,
};
use ruff_notebook::Notebook; use ruff_notebook::Notebook;
#[cfg(not(fuzzing))] #[cfg(not(fuzzing))]
use ruff_notebook::NotebookError; use ruff_notebook::NotebookError;
@ -24,7 +26,7 @@ use ruff_source_file::SourceFileBuilder;
use crate::codes::Rule; use crate::codes::Rule;
use crate::fix::{FixResult, fix_file}; use crate::fix::{FixResult, fix_file};
use crate::linter::check_path; use crate::linter::check_path;
use crate::message::{Emitter, EmitterContext, TextEmitter, create_syntax_error_diagnostic}; use crate::message::{EmitterContext, create_syntax_error_diagnostic};
use crate::package::PackageRoot; use crate::package::PackageRoot;
use crate::packaging::detect_package_root; use crate::packaging::detect_package_root;
use crate::settings::types::UnsafeFixes; use crate::settings::types::UnsafeFixes;
@ -444,42 +446,38 @@ pub(crate) fn print_jupyter_messages(
path: &Path, path: &Path,
notebook: &Notebook, notebook: &Notebook,
) -> String { ) -> String {
let mut output = Vec::new(); let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Full)
TextEmitter::default() .hide_severity(true)
.with_show_fix_status(true) .with_show_fix_status(true)
.with_show_fix_diff(true) .show_fix_diff(true)
.with_show_source(true) .with_fix_applicability(Applicability::DisplayOnly);
.with_fix_applicability(Applicability::DisplayOnly)
.emit(
&mut output,
diagnostics,
&EmitterContext::new(&FxHashMap::from_iter([(
path.file_name().unwrap().to_string_lossy().to_string(),
notebook.index().clone(),
)])),
)
.unwrap();
String::from_utf8(output).unwrap() DisplayDiagnostics::new(
&EmitterContext::new(&FxHashMap::from_iter([(
path.file_name().unwrap().to_string_lossy().to_string(),
notebook.index().clone(),
)])),
&config,
diagnostics,
)
.to_string()
} }
pub(crate) fn print_messages(diagnostics: &[Diagnostic]) -> String { pub(crate) fn print_messages(diagnostics: &[Diagnostic]) -> String {
let mut output = Vec::new(); let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Full)
TextEmitter::default() .hide_severity(true)
.with_show_fix_status(true) .with_show_fix_status(true)
.with_show_fix_diff(true) .show_fix_diff(true)
.with_show_source(true) .with_fix_applicability(Applicability::DisplayOnly);
.with_fix_applicability(Applicability::DisplayOnly)
.emit(
&mut output,
diagnostics,
&EmitterContext::new(&FxHashMap::default()),
)
.unwrap();
String::from_utf8(output).unwrap() DisplayDiagnostics::new(
&EmitterContext::new(&FxHashMap::default()),
&config,
diagnostics,
)
.to_string()
} }
#[macro_export] #[macro_export]