#![allow( clippy::collapsible_else_if, clippy::collapsible_if, clippy::implicit_hasher, clippy::match_same_arms, clippy::missing_errors_doc, clippy::missing_panics_doc, clippy::module_name_repetitions, clippy::must_use_candidate, clippy::similar_names, clippy::too_many_lines )] use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::sync::mpsc::channel; use std::time::Instant; use ::ruff::checks::{CheckCode, CheckKind}; use ::ruff::cli::{collect_per_file_ignores, extract_log_level, Cli}; use ::ruff::fs::iter_python_files; use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics}; use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::message::Message; use ::ruff::printer::Printer; use ::ruff::settings::configuration::Configuration; use ::ruff::settings::types::SerializationFormat; use ::ruff::settings::{pyproject, Settings}; #[cfg(feature = "update-informer")] use ::ruff::updates; use ::ruff::{cache, commands}; use anyhow::Result; use clap::{CommandFactory, Parser}; use colored::Colorize; use log::{debug, error}; use notify::{raw_watcher, RecursiveMode, Watcher}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; use ruff::autofix::fixer; use rustpython_ast::Location; use walkdir::DirEntry; /// Shim that calls `par_iter` except for wasm because there's no wasm support /// in rayon yet (there is a shim to be used for the web, but it requires js /// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator` /// so the signatures diverge #[cfg(not(target_family = "wasm"))] fn par_iter(iterable: &Vec) -> impl ParallelIterator { iterable.par_iter() } #[cfg(target_family = "wasm")] fn par_iter(iterable: &Vec) -> impl Iterator { iterable.iter() } fn read_from_stdin() -> Result { let mut buffer = String::new(); io::stdin().lock().read_to_string(&mut buffer)?; Ok(buffer) } fn run_once_stdin( settings: &Settings, filename: &Path, autofix: &fixer::Mode, ) -> Result { let stdin = read_from_stdin()?; let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?; diagnostics.messages.sort_unstable(); Ok(diagnostics) } fn run_once( files: &[PathBuf], settings: &Settings, cache: bool, autofix: &fixer::Mode, ) -> Diagnostics { // Collect all the files to check. let start = Instant::now(); let paths: Vec> = files .iter() .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .collect(); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); let start = Instant::now(); let mut diagnostics: Diagnostics = par_iter(&paths) .map(|entry| { match entry { Ok(entry) => { let path = entry.path(); lint_path(path, settings, &cache.into(), autofix) .map_err(|e| (Some(path.to_owned()), e.to_string())) } Err(e) => Err(( e.path().map(Path::to_owned), e.io_error() .map_or_else(|| e.to_string(), io::Error::to_string), )), } .unwrap_or_else(|(path, message)| { if let Some(path) = path { if settings.enabled.contains(&CheckCode::E902) { Diagnostics::new(vec![Message { kind: CheckKind::IOError(message), location: Location::default(), end_location: Location::default(), fix: None, filename: path.to_string_lossy().to_string(), source: None, }]) } else { error!("Failed to check {}: {message}", path.to_string_lossy()); Diagnostics::default() } } else { error!("{message}"); Diagnostics::default() } }) }) .reduce(Diagnostics::default, |mut acc, item| { acc += item; acc }); diagnostics.messages.sort_unstable(); let duration = start.elapsed(); debug!("Checked files in: {:?}", duration); diagnostics } fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize { // Collect all the files to check. let start = Instant::now(); let paths: Vec = files .iter() .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .flatten() .collect(); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); let start = Instant::now(); let modifications: usize = par_iter(&paths) .filter_map(|entry| { let path = entry.path(); match add_noqa_to_path(path, settings) { Ok(count) => Some(count), Err(e) => { error!("Failed to add noqa to {}: {e}", path.to_string_lossy()); None } } }) .sum(); let duration = start.elapsed(); debug!("Added noqa to files in: {:?}", duration); modifications } fn autoformat(files: &[PathBuf], settings: &Settings) -> usize { // Collect all the files to format. let start = Instant::now(); let paths: Vec = files .iter() .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .flatten() .collect(); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); let start = Instant::now(); let modifications = par_iter(&paths) .filter_map(|entry| { let path = entry.path(); match autoformat_path(path) { Ok(()) => Some(()), Err(e) => { error!("Failed to autoformat {}: {e}", path.to_string_lossy()); None } } }) .count(); let duration = start.elapsed(); debug!("Auto-formatted files in: {:?}", duration); modifications } fn inner_main() -> Result { // Extract command-line arguments. let cli = Cli::parse(); let fix = cli.fix(); let log_level = extract_log_level(&cli); set_up_logging(&log_level)?; if let Some(shell) = cli.generate_shell_completion { shell.generate(&mut Cli::command(), &mut std::io::stdout()); return Ok(ExitCode::SUCCESS); } // Find the project root and pyproject.toml. let project_root = pyproject::find_project_root(&cli.files); let pyproject = cli .config .or_else(|| pyproject::find_pyproject_toml(project_root.as_ref())); // Reconcile configuration from pyproject.toml and command-line arguments. let mut configuration = Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?; if !cli.exclude.is_empty() { configuration.exclude = cli.exclude; } if !cli.extend_exclude.is_empty() { configuration.extend_exclude = cli.extend_exclude; } if !cli.per_file_ignores.is_empty() { configuration.per_file_ignores = collect_per_file_ignores(cli.per_file_ignores); } if !cli.select.is_empty() { configuration.select = cli.select; } if !cli.extend_select.is_empty() { configuration.extend_select = cli.extend_select; } if !cli.ignore.is_empty() { configuration.ignore = cli.ignore; } if !cli.extend_ignore.is_empty() { configuration.extend_ignore = cli.extend_ignore; } if !cli.fixable.is_empty() { configuration.fixable = cli.fixable; } if !cli.unfixable.is_empty() { configuration.unfixable = cli.unfixable; } if let Some(format) = cli.format { configuration.format = format; } if let Some(line_length) = cli.line_length { configuration.line_length = line_length; } if let Some(max_complexity) = cli.max_complexity { configuration.mccabe.max_complexity = max_complexity; } if let Some(target_version) = cli.target_version { configuration.target_version = target_version; } if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx { configuration.dummy_variable_rgx = dummy_variable_rgx; } if let Some(fix) = fix { configuration.fix = fix; } if cli.show_source { configuration.show_source = true; } if cli.show_settings && cli.show_files { eprintln!("Error: specify --show-settings or show-files (not both)."); return Ok(ExitCode::FAILURE); } if cli.show_settings { commands::show_settings(&configuration, project_root.as_ref(), pyproject.as_ref()); return Ok(ExitCode::SUCCESS); } // Extract settings for internal use. let autofix = if configuration.fix { fixer::Mode::Apply } else if matches!(configuration.format, SerializationFormat::Json) { fixer::Mode::Generate } else { fixer::Mode::None }; let settings = Settings::from_configuration(configuration, project_root.as_ref())?; // Now that we've inferred the appropriate log level, add some debug // information. match &project_root { Some(path) => debug!("Found project root at: {:?}", path), None => debug!("Unable to identify project root; assuming current directory..."), }; match &pyproject { Some(path) => debug!("Found pyproject.toml at: {:?}", path), None => debug!("Unable to find pyproject.toml; using default settings..."), }; if let Some(code) = cli.explain { commands::explain(&code, settings.format)?; return Ok(ExitCode::SUCCESS); } if cli.show_files { commands::show_files(&cli.files, &settings); return Ok(ExitCode::SUCCESS); } // Initialize the cache. let mut cache_enabled: bool = !cli.no_cache; if cache_enabled && cache::init().is_err() { eprintln!("Unable to initialize cache; disabling..."); cache_enabled = false; } let printer = Printer::new(&settings.format, &log_level); if cli.watch { if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) { eprintln!("Warning: --fix is not enabled in watch mode."); } if cli.add_noqa { eprintln!("Warning: --no-qa is not enabled in watch mode."); } if cli.autoformat { eprintln!("Warning: --autoformat is not enabled in watch mode."); } if settings.format != SerializationFormat::Text { eprintln!("Warning: --format 'text' is used in watch mode."); } // Perform an initial run instantly. printer.clear_screen()?; printer.write_to_user("Starting linter in watch mode...\n"); let messages = run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None); printer.write_continuously(&messages)?; // Configure the file watcher. let (tx, rx) = channel(); let mut watcher = raw_watcher(tx)?; for file in &cli.files { watcher.watch(file, RecursiveMode::Recursive)?; } loop { match rx.recv() { Ok(e) => { if let Some(path) = e.path { if path.to_string_lossy().ends_with(".py") { printer.clear_screen()?; printer.write_to_user("File change detected...\n"); let messages = run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None); printer.write_continuously(&messages)?; } } } Err(e) => return Err(e.into()), } } } else if cli.add_noqa { let modifications = add_noqa(&cli.files, &settings); 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 && log_level >= LogLevel::Default { println!("Formatted {modifications} files."); } } else { let is_stdin = cli.files == vec![PathBuf::from("-")]; // Generate lint violations. let diagnostics = if is_stdin { let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string()); let path = Path::new(&filename); run_once_stdin(&settings, path, &autofix)? } else { run_once(&cli.files, &settings, cache_enabled, &autofix) }; // 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 && matches!(autofix, fixer::Mode::Apply)) { printer.write_once(&diagnostics)?; } // Check for updates if we're in a non-silent log level. #[cfg(feature = "update-informer")] if !is_stdin && log_level >= LogLevel::Default && atty::is(atty::Stream::Stdout) { drop(updates::check_for_updates()); } if !diagnostics.messages.is_empty() && !cli.exit_zero { return Ok(ExitCode::FAILURE); } } Ok(ExitCode::SUCCESS) } fn main() -> ExitCode { match inner_main() { Ok(code) => code, Err(err) => { eprintln!("{} {err:?}", "error".red().bold()); ExitCode::FAILURE } } }