use std::io; use std::path::{Path, PathBuf}; use std::time::Instant; use anyhow::Result; use colored::Colorize; use ignore::Error; use log::{debug, error, warn}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; use ruff_text_size::{TextRange, TextSize}; use ruff::message::Message; use ruff::registry::Rule; use ruff::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; use ruff::settings::{flags, AllSettings}; use ruff::{fs, packaging, resolver, warn_user_once, IOError}; use ruff_diagnostics::Diagnostic; use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; use crate::args::Overrides; use crate::cache; use crate::diagnostics::Diagnostics; use crate::panic::catch_unwind; /// Run the linter over a collection of files. pub(crate) fn run( files: &[PathBuf], pyproject_config: &PyprojectConfig, overrides: &Overrides, cache: flags::Cache, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { // Collect all the Python files to check. let start = Instant::now(); let (paths, resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); return Ok(Diagnostics::default()); } // Initialize the cache. if cache.into() { fn init_cache(path: &Path) { if let Err(e) = cache::init(path) { error!("Failed to initialize cache at {}: {e:?}", path.display()); } } match pyproject_config.strategy { PyprojectDiscoveryStrategy::Fixed => { init_cache(&pyproject_config.settings.cli.cache_dir); } PyprojectDiscoveryStrategy::Hierarchical => { for settings in std::iter::once(&pyproject_config.settings).chain(resolver.iter()) { init_cache(&settings.cli.cache_dir); } } } }; // Discover the package root for each Python file. let package_roots = packaging::detect_package_roots( &paths .iter() .flatten() .map(ignore::DirEntry::path) .collect::>(), &resolver, pyproject_config, ); let start = Instant::now(); let mut diagnostics: Diagnostics = paths .par_iter() .map(|entry| { match entry { Ok(entry) => { let path = entry.path(); let package = path .parent() .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); let settings = resolver.resolve_all(path, pyproject_config); lint_path(path, package, settings, cache, noqa, autofix).map_err(|e| { (Some(path.to_owned()), { let mut error = e.to_string(); for cause in e.chain() { error += &format!("\n Caused by: {cause}"); } error }) }) } Err(e) => Err(( if let Error::WithPath { path, .. } = e { Some(path.clone()) } else { None }, e.io_error() .map_or_else(|| e.to_string(), io::Error::to_string), )), } .unwrap_or_else(|(path, message)| { if let Some(path) = &path { error!( "{}{}{} {message}", "Failed to lint ".bold(), fs::relativize_path(path).bold(), ":".bold() ); let settings = resolver.resolve(path, pyproject_config); if settings.rules.enabled(Rule::IOError) { let file = SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(); Diagnostics::new( vec![Message::from_diagnostic( Diagnostic::new(IOError { message }, TextRange::default()), file, TextSize::default(), )], ImportMap::default(), ) } else { Diagnostics::default() } } else { error!("{} {message}", "Encountered error:".bold()); Diagnostics::default() } }) }) .reduce(Diagnostics::default, |mut acc, item| { acc += item; acc }); diagnostics.messages.sort(); let duration = start.elapsed(); debug!("Checked {:?} files in: {:?}", paths.len(), duration); Ok(diagnostics) } /// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits /// a diagnostic if the linting the file panics. fn lint_path( path: &Path, package: Option<&Path>, settings: &AllSettings, cache: flags::Cache, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { let result = catch_unwind(|| { crate::diagnostics::lint_path(path, package, settings, cache, noqa, autofix) }); match result { Ok(inner) => inner, Err(error) => { let message = r#"This indicates a bug in `ruff`. If you could open an issue at: https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative! "#; warn!( "{}{}{} {message}\n{error}", "Linting panicked ".bold(), fs::relativize_path(path).bold(), ":".bold() ); Ok(Diagnostics::default()) } } } #[cfg(test)] #[cfg(feature = "jupyter_notebook")] mod test { use std::path::PathBuf; use std::str::FromStr; use anyhow::Result; use path_absolutize::Absolutize; use ruff::logging::LogLevel; use ruff::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; use ruff::settings::configuration::{Configuration, RuleSelection}; use ruff::settings::flags::FixMode; use ruff::settings::flags::{Cache, Noqa}; use ruff::settings::types::SerializationFormat; use ruff::settings::AllSettings; use ruff::RuleSelector; use crate::args::Overrides; use crate::printer::{Flags, Printer}; use super::run; #[test] fn test_jupyter_notebook_integration() -> Result<()> { let overrides: Overrides = Overrides { select: Some(vec![ RuleSelector::from_str("B")?, RuleSelector::from_str("F")?, ]), ..Default::default() }; let mut configuration = Configuration::default(); configuration.rule_selections.push(RuleSelection { select: Some(vec![ RuleSelector::from_str("B")?, RuleSelector::from_str("F")?, ]), ..Default::default() }); let root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("ruff") .join("resources") .join("test") .join("fixtures") .join("jupyter"); let diagnostics = run( &[root_path.join("valid.ipynb")], &PyprojectConfig::new( PyprojectDiscoveryStrategy::Fixed, AllSettings::from_configuration(configuration, &root_path)?, None, ), &overrides, Cache::Disabled, Noqa::Enabled, FixMode::None, )?; let printer = Printer::new( SerializationFormat::Text, LogLevel::Default, FixMode::None, Flags::SHOW_VIOLATIONS, ); let mut writer: Vec = Vec::new(); // Mute the terminal color codes colored::control::set_override(false); printer.write_once(&diagnostics, &mut writer)?; // TODO(konstin): Set jupyter notebooks as none-fixable for now // TODO(konstin) 2: Make jupyter notebooks fixable let expected = format!( "{valid_ipynb}:cell 1:2:5: F841 [*] Local variable `x` is assigned to but never used {valid_ipynb}:cell 3:1:24: B006 Do not use mutable data structures for argument defaults Found 2 errors. [*] 1 potentially fixable with the --fix option. ", valid_ipynb = root_path.join("valid.ipynb").absolutize()?.display() ); assert_eq!(expected, String::from_utf8(writer)?); Ok(()) } }