diff --git a/src/lib.rs b/src/lib.rs index 600d7f9ec3..a14e6e2d3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod fs; pub mod linter; pub mod logging; pub mod message; +pub mod printer; mod pyproject; mod python; pub mod settings; diff --git a/src/main.rs b/src/main.rs index 485ddefcfb..b2830a253d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ use ::ruff::fs::iter_python_files; use ::ruff::linter::lint_path; use ::ruff::logging::set_up_logging; use ::ruff::message::Message; +use ::ruff::printer::{Printer, SerializationFormat}; use ::ruff::settings::Settings; use ::ruff::tell_user; @@ -60,6 +61,9 @@ struct Cli { /// Like --exclude, but adds additional files and directories on top of the excluded ones. #[clap(long, multiple = true)] extend_exclude: Vec, + /// Output formatting of linting messages + #[clap(long, arg_enum, default_value_t=SerializationFormat::Text)] + format: SerializationFormat, } #[cfg(feature = "update-informer")] @@ -142,60 +146,15 @@ fn run_once( Ok(messages) } -fn report_once(messages: &[Message]) -> Result<()> { - let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) = - messages.iter().partition(|message| message.fixed); - let num_fixable = outstanding - .iter() - .filter(|message| message.kind.fixable()) - .count(); - - if !outstanding.is_empty() { - for message in &outstanding { - println!("{}", message); - } - println!(); - } - - if !fixed.is_empty() { - println!( - "Found {} error(s) ({} fixed).", - outstanding.len(), - fixed.len() - ); - } else { - println!("Found {} error(s).", outstanding.len()); - } - - if num_fixable > 0 { - println!("{num_fixable} potentially fixable with the --fix option."); - } - - Ok(()) -} - -fn report_continuously(messages: &[Message]) -> Result<()> { - tell_user!( - "Found {} error(s). Watching for file changes.", - messages.len(), - ); - - if !messages.is_empty() { - println!(); - for message in messages { - println!("{}", message); - } - } - - Ok(()) -} - fn inner_main() -> Result { let cli = Cli::parse(); set_up_logging(cli.verbose)?; let mut settings = Settings::from_paths(&cli.files); + + let mut printer = Printer::new(cli.format); + if !cli.select.is_empty() { settings.select(cli.select); } @@ -211,16 +170,20 @@ fn inner_main() -> Result { if cli.watch { if cli.fix { - println!("Warning: --fix is not enabled in watch mode.") + println!("Warning: --fix is not enabled in watch mode."); + } + + if cli.format != SerializationFormat::Text { + println!("Warning: --format 'text' is used in watch mode."); } // Perform an initial run instantly. - clearscreen::clear()?; + printer.clear_screen()?; tell_user!("Starting linter in watch mode...\n"); let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?; if !cli.quiet { - report_continuously(&messages)?; + printer.write_continuously(&messages)?; } // Configure the file watcher. @@ -235,12 +198,12 @@ fn inner_main() -> Result { Ok(e) => { if let Some(path) = e.path { if path.to_string_lossy().ends_with(".py") { - clearscreen::clear()?; + printer.clear_screen()?; tell_user!("File change detected...\n"); let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?; if !cli.quiet { - report_continuously(&messages)?; + printer.write_continuously(&messages)?; } } } @@ -251,7 +214,7 @@ fn inner_main() -> Result { } else { let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?; if !cli.quiet { - report_once(&messages)?; + printer.write_once(&messages)?; } #[cfg(feature = "update-informer")] diff --git a/src/printer.rs b/src/printer.rs new file mode 100644 index 0000000000..625be3747c --- /dev/null +++ b/src/printer.rs @@ -0,0 +1,80 @@ +use colored::Colorize; + +use anyhow::Result; +use clap::ValueEnum; + +use crate::message::Message; +use crate::tell_user; + +#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)] +pub enum SerializationFormat { + Text, + Json, +} + +pub struct Printer { + format: SerializationFormat, +} + +impl Printer { + pub fn new(format: SerializationFormat) -> Self { + Self { format } + } + + pub fn write_once(&mut self, messages: &[Message]) -> Result<()> { + let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) = + messages.iter().partition(|message| message.fixed); + let num_fixable = outstanding + .iter() + .filter(|message| message.kind.fixable()) + .count(); + + match self.format { + SerializationFormat::Json => { + println!("{}", serde_json::to_string_pretty(&messages)?) + } + SerializationFormat::Text => { + if !fixed.is_empty() { + println!( + "Found {} error(s) ({} fixed).", + outstanding.len(), + fixed.len() + ) + } else { + println!("Found {} error(s).", outstanding.len()) + } + + for message in outstanding { + println!("{}", message) + } + + if num_fixable > 0 { + println!("{num_fixable} potentially fixable with the --fix option.") + } + } + } + + Ok(()) + } + + pub fn write_continuously(&mut self, messages: &[Message]) -> Result<()> { + tell_user!( + "Found {} error(s). Watching for file changes.", + messages.len(), + ); + + if !messages.is_empty() { + println!(); + for message in messages { + println!("{}", message) + } + } + + Ok(()) + } + + pub fn clear_screen(&mut self) -> Result<()> { + clearscreen::clear()?; + Ok(()) + } +}