diff --git a/src/cli.rs b/src/cli.rs index 76780f64da..317b931e84 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,7 @@ use log::warn; use regex::Regex; use crate::checks_gen::CheckCodePrefix; +use crate::logging::LogLevel; use crate::printer::SerializationFormat; use crate::settings::configuration::Configuration; use crate::settings::types::{PatternPrefixPair, PythonVersion}; @@ -93,6 +94,19 @@ pub struct Cli { pub stdin_filename: Option, } +/// Map the CLI settings to a `LogLevel`. +pub fn extract_log_level(cli: &Cli) -> LogLevel { + if cli.silent { + LogLevel::Silent + } else if cli.quiet { + LogLevel::Quiet + } else if cli.verbose { + LogLevel::Verbose + } else { + LogLevel::Default + } +} + pub enum Warnable { Select, ExtendSelect, diff --git a/src/logging.rs b/src/logging.rs index 15d3183d7e..6544c43744 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -15,7 +15,36 @@ macro_rules! tell_user { } } -pub fn set_up_logging(verbose: bool) -> Result<()> { +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)] +pub enum LogLevel { + // No output (+ `log::LevelFilter::Off`). + Silent, + // Only show lint violations, with no decorative output (+ `log::LevelFilter::Off`). + Quiet, + // All user-facing output (+ `log::LevelFilter::Info`). + Default, + // All user-facing output (+ `log::LevelFilter::Debug`). + Verbose, +} + +impl LogLevel { + fn level_filter(&self) -> log::LevelFilter { + match self { + LogLevel::Default => log::LevelFilter::Info, + LogLevel::Verbose => log::LevelFilter::Debug, + LogLevel::Quiet => log::LevelFilter::Off, + LogLevel::Silent => log::LevelFilter::Off, + } + } +} + +impl Default for LogLevel { + fn default() -> Self { + LogLevel::Default + } +} + +pub fn set_up_logging(level: &LogLevel) -> Result<()> { fern::Dispatch::new() .format(|out, message, record| { out.finish(format_args!( @@ -26,12 +55,22 @@ pub fn set_up_logging(verbose: bool) -> Result<()> { message )) }) - .level(if verbose { - log::LevelFilter::Debug - } else { - log::LevelFilter::Info - }) + .level(level.level_filter()) .chain(std::io::stdout()) .apply() .map_err(|e| e.into()) } + +#[cfg(test)] +mod tests { + use crate::logging::LogLevel; + + #[test] + fn ordering() { + assert!(LogLevel::Default > LogLevel::Silent); + assert!(LogLevel::Default >= LogLevel::Default); + assert!(LogLevel::Quiet > LogLevel::Silent); + assert!(LogLevel::Verbose > LogLevel::Default); + assert!(LogLevel::Verbose > LogLevel::Silent); + } +} diff --git a/src/main.rs b/src/main.rs index be029438f3..831fd0950a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,17 +16,16 @@ use rayon::prelude::*; use ruff::cache; use ruff::checks::{CheckCode, CheckKind}; use ruff::checks_gen::CheckCodePrefix; -use ruff::cli::{collect_per_file_ignores, warn_on, Cli, Warnable}; +use ruff::cli::{collect_per_file_ignores, extract_log_level, warn_on, Cli, Warnable}; use ruff::fs::iter_python_files; use ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin}; -use ruff::logging::set_up_logging; +use ruff::logging::{set_up_logging, LogLevel}; use ruff::message::Message; use ruff::printer::{Printer, SerializationFormat}; use ruff::settings::configuration::Configuration; use ruff::settings::types::FilePattern; use ruff::settings::user::UserConfiguration; use ruff::settings::{pyproject, Settings}; -use ruff::tell_user; use walkdir::DirEntry; #[cfg(feature = "update-informer")] @@ -223,10 +222,10 @@ fn autoformat(files: &[PathBuf], settings: &Settings) -> Result { } fn inner_main() -> Result { - let mut cli = Cli::parse(); - cli.quiet |= cli.silent; + let cli = Cli::parse(); - set_up_logging(cli.verbose)?; + let log_level = extract_log_level(&cli); + set_up_logging(&log_level)?; // Find the project root and pyproject.toml. let project_root = pyproject::find_project_root(&cli.files); @@ -320,7 +319,7 @@ fn inner_main() -> Result { #[cfg(not(target_family = "wasm"))] cache::init()?; - let mut printer = Printer::new(cli.format, cli.verbose); + let printer = Printer::new(&cli.format, &log_level); if cli.watch { if cli.fix { eprintln!("Warning: --fix is not enabled in watch mode."); @@ -340,12 +339,10 @@ fn inner_main() -> Result { // Perform an initial run instantly. printer.clear_screen()?; - tell_user!("Starting linter in watch mode...\n"); + printer.write_to_user("Starting linter in watch mode...\n"); let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?; - if !cli.silent { - printer.write_continuously(&messages)?; - } + printer.write_continuously(&messages)?; // Configure the file watcher. let (tx, rx) = channel(); @@ -360,12 +357,10 @@ fn inner_main() -> Result { if let Some(path) = e.path { if path.to_string_lossy().ends_with(".py") { printer.clear_screen()?; - tell_user!("File change detected...\n"); + printer.write_to_user("File change detected...\n"); let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?; - if !cli.silent { - printer.write_continuously(&messages)?; - } + printer.write_continuously(&messages)?; } } } @@ -374,37 +369,36 @@ fn inner_main() -> Result { } } else if cli.add_noqa { let modifications = add_noqa(&cli.files, &settings)?; - if modifications > 0 { + if modifications > 0 && log_level >= LogLevel::Default { println!("Added {modifications} noqa directives."); } } else if cli.autoformat { let modifications = autoformat(&cli.files, &settings)?; - if modifications > 0 { + if modifications > 0 && log_level >= LogLevel::Default { println!("Formatted {modifications} files."); } } else { - let (messages, should_print_messages, should_check_updates) = - if cli.files == vec![PathBuf::from("-")] { - let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string()); - let path = Path::new(&filename); - ( - run_once_stdin(&settings, path, cli.fix)?, - !cli.silent && !cli.fix, - false, - ) - } else { - ( - run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?, - !cli.silent, - !cli.quiet, - ) - }; - if should_print_messages { + let is_stdin = cli.files == vec![PathBuf::from("-")]; + + // Generate lint violations. + let messages = if is_stdin { + let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string()); + let path = Path::new(&filename); + run_once_stdin(&settings, path, cli.fix)? + } else { + run_once(&cli.files, &settings, !cli.no_cache, cli.fix)? + }; + + // Always try to print violations (the printer itself may suppress output), + // unless we're writing fixes via stdin (in which case, the transformed + // source code goes to stdout). + if !(is_stdin && cli.fix) { printer.write_once(&messages)?; } + // Check for updates if we're in a non-silent log level. #[cfg(feature = "update-informer")] - if should_check_updates { + if !is_stdin && log_level >= LogLevel::Default { check_for_updates(); } diff --git a/src/printer.rs b/src/printer.rs index 8d9690014c..e2e9b6cd5b 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -5,6 +5,7 @@ use rustpython_parser::ast::Location; use serde::Serialize; use crate::checks::{CheckCode, CheckKind}; +use crate::logging::LogLevel; use crate::message::Message; use crate::tell_user; @@ -25,17 +26,27 @@ struct ExpandedMessage<'a> { filename: &'a String, } -pub struct Printer { - format: SerializationFormat, - verbose: bool, +pub struct Printer<'a> { + format: &'a SerializationFormat, + log_level: &'a LogLevel, } -impl Printer { - pub fn new(format: SerializationFormat, verbose: bool) -> Self { - Self { format, verbose } +impl<'a> Printer<'a> { + pub fn new(format: &'a SerializationFormat, log_level: &'a LogLevel) -> Self { + Self { format, log_level } } - pub fn write_once(&mut self, messages: &[Message]) -> Result<()> { + pub fn write_to_user(&self, message: &str) { + if self.log_level >= &LogLevel::Default { + tell_user!("{}", message); + } + } + + pub fn write_once(&self, messages: &[Message]) -> Result<()> { + if matches!(self.log_level, LogLevel::Silent) { + return Ok(()); + } + let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) = messages.iter().partition(|message| message.fixed); let num_fixable = outstanding @@ -64,22 +75,26 @@ impl Printer { ) } SerializationFormat::Text => { - if !fixed.is_empty() { - println!( - "Found {} error(s) ({} fixed).", - outstanding.len(), - fixed.len() - ) - } else if !outstanding.is_empty() || self.verbose { - println!("Found {} error(s).", outstanding.len()) + if self.log_level >= &LogLevel::Default { + if !fixed.is_empty() { + println!( + "Found {} error(s) ({} fixed).", + outstanding.len(), + fixed.len() + ) + } else if !outstanding.is_empty() { + 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.") + if self.log_level >= &LogLevel::Default { + if num_fixable > 0 { + println!("{num_fixable} potentially fixable with the --fix option.") + } } } } @@ -87,14 +102,22 @@ impl Printer { Ok(()) } - pub fn write_continuously(&mut self, messages: &[Message]) -> Result<()> { - tell_user!( - "Found {} error(s). Watching for file changes.", - messages.len(), - ); + pub fn write_continuously(&self, messages: &[Message]) -> Result<()> { + if matches!(self.log_level, LogLevel::Silent) { + return Ok(()); + } + + if self.log_level >= &LogLevel::Default { + tell_user!( + "Found {} error(s). Watching for file changes.", + messages.len(), + ); + } if !messages.is_empty() { - println!(); + if self.log_level >= &LogLevel::Default { + println!(); + } for message in messages { println!("{}", message) } @@ -103,7 +126,7 @@ impl Printer { Ok(()) } - pub fn clear_screen(&mut self) -> Result<()> { + pub fn clear_screen(&self) -> Result<()> { #[cfg(not(target_family = "wasm"))] clearscreen::clear()?; Ok(())