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/logging.rs b/src/logging.rs index 15d3183d7e..af89aa5a17 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -3,15 +3,16 @@ use fern; #[macro_export] macro_rules! tell_user { - ($($arg:tt)*) => { - println!( + ($writer:expr,$($arg:tt)*) => { + writeln!( + $writer, "[{}] {}", chrono::Local::now() .format("%H:%M:%S %p") .to_string() .dimmed(), format_args!($($arg)*) - ) + )? } } diff --git a/src/main.rs b/src/main.rs index 1ef8f30db2..131569504b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use std::io::{stdout, BufWriter, Write}; use std::path::PathBuf; use std::process::ExitCode; use std::sync::mpsc::channel; @@ -18,6 +19,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; @@ -57,6 +59,9 @@ struct Cli { /// List of file and/or directory patterns to exclude from checks. #[clap(long, multiple = true)] exclude: Vec, + /// Output formatting of linting messages + #[clap(long, arg_enum, default_value_t=SerializationFormat::Text)] + format: SerializationFormat, } #[cfg(feature = "update-informer")] @@ -130,60 +135,14 @@ 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(BufWriter::new(stdout()), cli.format); + if !cli.select.is_empty() { settings.select(cli.select); } @@ -201,11 +160,11 @@ fn inner_main() -> Result { // Perform an initial run instantly. clearscreen::clear()?; - tell_user!("Starting linter in watch mode...\n"); + tell_user!(printer.writer, "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. @@ -221,11 +180,11 @@ fn inner_main() -> Result { if let Some(path) = e.path { if path.to_string_lossy().ends_with(".py") { clearscreen::clear()?; - tell_user!("File change detected...\n"); + tell_user!(printer.writer, "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)?; } } } @@ -236,7 +195,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..6d7a701ffa --- /dev/null +++ b/src/printer.rs @@ -0,0 +1,82 @@ +use colored::Colorize; +use std::io::Write; + +use anyhow::Result; +use clap::ValueEnum; + +use crate::message::Message; +use crate::tell_user; + +#[derive(Clone, ValueEnum, PartialEq, Eq, Debug)] +pub enum SerializationFormat { + Text, + Json, +} + +pub struct Printer { + pub writer: W, + format: SerializationFormat, +} + +impl Printer { + pub fn new(writer: W, format: SerializationFormat) -> Self { + Self { writer, 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 => { + writeln!(self.writer, "{}", serde_json::to_string_pretty(&messages)?)? + } + SerializationFormat::Text => { + if !fixed.is_empty() { + writeln!( + self.writer, + "Found {} error(s) ({} fixed).", + outstanding.len(), + fixed.len() + )? + } else { + writeln!(self.writer, "Found {} error(s).", outstanding.len())? + } + + for message in outstanding { + writeln!(self.writer, "{}", message)? + } + + if num_fixable > 0 { + writeln!( + self.writer, + "{num_fixable} potentially fixable with the --fix option." + )? + } + } + } + + Ok(()) + } + + pub fn write_continuously(&mut self, messages: &[Message]) -> Result<()> { + tell_user!( + self.writer, + "Found {} error(s). Watching for file changes.", + messages.len(), + ); + + if !messages.is_empty() { + writeln!(self.writer, "\n")?; + for message in messages { + writeln!(self.writer, "{}", message)? + } + } + + Ok(()) + } +}