diff --git a/README.md b/README.md index 71fd30bc1bf5b..6bbdde75ad132 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,8 @@ Options: Show violations with source code --respect-gitignore Respect file exclusions via `.gitignore` and other standard ignore files + --force-exclude + Enforce exclusions, even for paths passed to Ruff directly on the command-line --show-files See the files Ruff will be run against with the current settings --show-settings @@ -336,6 +338,8 @@ Options: The name of the file when passing it through stdin --explain Explain a rule + --cache-dir + Path to the cache directory -h, --help Print help information -V, --version @@ -1595,6 +1599,31 @@ allowed-confusables = ["−", "ρ", "∗"] --- +#### [`cache-dir`](#cache-dir) + +A path to the cache directory. + +By default, Ruff stores cache results in a `.ruff_cache` directory in the current +project root. + +However, Ruff will also respect the `RUFF_CACHE_DIR` environment variable, which takes +precedence over that default. + +This setting will override even the `RUFF_CACHE_DIR` environment variable, if set. + +**Default value**: `.ruff_cache` + +**Type**: `PathBuf` + +**Example usage**: + +```toml +[tool.ruff] +cache-dir = "~/.cache/ruff" +``` + +--- + #### [`dummy-variable-rgx`](#dummy-variable-rgx) A regular expression used to identify "dummy" variables, or those which should be diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index 66ab8303b3fc3..a33165ff2f6da 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -315,6 +315,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -369,6 +370,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -423,6 +425,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -477,6 +480,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -531,6 +535,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -629,6 +634,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -684,6 +690,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, diff --git a/src/cache.rs b/src/cache.rs index 665a51bd14f36..b5fef6e31f61c 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -3,7 +3,7 @@ use std::fs; use std::fs::{create_dir_all, File, Metadata}; use std::hash::{Hash, Hasher}; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::Result; use filetime::FileTime; @@ -36,8 +36,12 @@ struct CheckResult { messages: Vec, } -fn cache_dir() -> &'static Path { - Path::new(CACHE_DIR.as_ref().map_or(".ruff_cache", String::as_str)) +/// Return the cache directory for a given project root. Defers to the +/// `RUFF_CACHE_DIR` environment variable, if set. +pub fn cache_dir(project_root: &Path) -> PathBuf { + CACHE_DIR + .as_ref() + .map_or_else(|| project_root.join(".ruff_cache"), PathBuf::from) } fn content_dir() -> &'static Path { @@ -53,10 +57,8 @@ fn cache_key>(path: P, settings: &Settings, autofix: fixer::Mode) hasher.finish() } -/// Initialize the cache directory. -pub fn init() -> Result<()> { - let path = cache_dir(); - +/// Initialize the cache at the specified `Path`. +pub fn init(path: &Path) -> Result<()> { // Create the cache directories. create_dir_all(path.join(content_dir()))?; @@ -75,15 +77,15 @@ pub fn init() -> Result<()> { Ok(()) } -fn write_sync(key: u64, value: &[u8]) -> Result<(), std::io::Error> { +fn write_sync(cache_dir: &Path, key: u64, value: &[u8]) -> Result<(), std::io::Error> { fs::write( - cache_dir().join(content_dir()).join(format!("{key:x}")), + cache_dir.join(content_dir()).join(format!("{key:x}")), value, ) } -fn read_sync(key: u64) -> Result, std::io::Error> { - fs::read(cache_dir().join(content_dir()).join(format!("{key:x}"))) +fn read_sync(cache_dir: &Path, key: u64) -> Result, std::io::Error> { + fs::read(cache_dir.join(content_dir()).join(format!("{key:x}"))) } /// Get a value from the cache. @@ -98,7 +100,7 @@ pub fn get>( return None; }; - let encoded = read_sync(cache_key(path, settings, autofix)).ok()?; + let encoded = read_sync(&settings.cache_dir, cache_key(path, settings, autofix)).ok()?; let (mtime, messages) = match bincode::deserialize::(&encoded[..]) { Ok(CheckResult { metadata: CacheMetadata { mtime }, @@ -135,6 +137,7 @@ pub fn set>( messages, }; if let Err(e) = write_sync( + &settings.cache_dir, cache_key(path, settings, autofix), &bincode::serialize(&check_result).unwrap(), ) { diff --git a/src/cli.rs b/src/cli.rs index f48a1212089c2..b9b7cd80e5717 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -133,6 +133,9 @@ pub struct Cli { /// Generate shell completion #[arg(long, hide = true, value_name = "SHELL")] pub generate_shell_completion: Option, + /// Path to the cache directory. + #[arg(long)] + pub cache_dir: Option, } impl Cli { @@ -180,6 +183,7 @@ impl Cli { fix: resolve_bool_arg(self.fix, self.no_fix), format: self.format, force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude), + cache_dir: self.cache_dir, }, ) } @@ -238,6 +242,7 @@ pub struct Overrides { pub fix: Option, pub format: Option, pub force_exclude: Option, + pub cache_dir: Option, } /// Map the CLI settings to a `LogLevel`. diff --git a/src/commands.rs b/src/commands.rs index d06e2ecb6f9b6..1205a86133e80 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -20,7 +20,7 @@ use crate::message::Message; use crate::resolver::{FileDiscovery, PyprojectDiscovery}; use crate::settings::flags; use crate::settings::types::SerializationFormat; -use crate::{packages, resolver}; +use crate::{cache, packages, resolver}; /// Run the linter over a collection of files. pub fn run( @@ -47,6 +47,30 @@ pub fn run( .collect::>(), ); + // Initialize the cache. + if matches!(cache, flags::Cache::Enabled) { + match &pyproject_strategy { + PyprojectDiscovery::Fixed(settings) => { + if let Err(e) = cache::init(&settings.cache_dir) { + error!( + "Failed to initialize cache at {}: {e:?}", + settings.cache_dir.to_string_lossy() + ); + } + } + PyprojectDiscovery::Hierarchical(default) => { + for settings in std::iter::once(default).chain(resolver.iter()) { + if let Err(e) = cache::init(&settings.cache_dir) { + error!( + "Failed to initialize cache at {}: {e:?}", + settings.cache_dir.to_string_lossy() + ); + } + } + } + } + }; + let start = Instant::now(); let mut diagnostics: Diagnostics = par_iter(&paths) .map(|entry| { diff --git a/src/main.rs b/src/main.rs index fa933e91dffbd..4c790757164e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ use std::sync::mpsc::channel; use ::ruff::autofix::fixer; use ::ruff::cli::{extract_log_level, Cli, Overrides}; +use ::ruff::commands; use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::printer::Printer; use ::ruff::resolver::{resolve_settings, FileDiscovery, PyprojectDiscovery, Relativity}; @@ -26,7 +27,6 @@ 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; @@ -123,6 +123,7 @@ fn inner_main() -> Result { } else { fixer::Mode::None }; + let cache = !cli.no_cache; if let Some(code) = cli.explain { commands::explain(&code, &format)?; @@ -137,13 +138,6 @@ fn inner_main() -> Result { 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(&format, &log_level); if cli.watch { if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) { @@ -168,7 +162,7 @@ fn inner_main() -> Result { &pyproject_strategy, &file_strategy, &overrides, - cache_enabled.into(), + cache.into(), fixer::Mode::None, )?; printer.write_continuously(&messages)?; @@ -198,7 +192,7 @@ fn inner_main() -> Result { &pyproject_strategy, &file_strategy, &overrides, - cache_enabled.into(), + cache.into(), fixer::Mode::None, )?; printer.write_continuously(&messages)?; @@ -237,7 +231,7 @@ fn inner_main() -> Result { &pyproject_strategy, &file_strategy, &overrides, - cache_enabled.into(), + cache.into(), autofix, )? }; diff --git a/src/resolver.rs b/src/resolver.rs index bf103c6b6e921..c3651d082e4f8 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -86,6 +86,11 @@ impl Resolver { .unwrap_or(default), } } + + /// Return an iterator over the resolved `Settings` in this `Resolver`. + pub fn iter(&self) -> impl Iterator { + self.settings.values() + } } /// Recursively resolve a `Configuration` from a `pyproject.toml` file at the diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index aaeba6c1ec898..632d161588a5f 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -46,6 +46,7 @@ pub struct Configuration { pub src: Option>, pub target_version: Option, pub unfixable: Option>, + pub cache_dir: Option, // Plugins pub flake8_annotations: Option, pub flake8_bugbear: Option, @@ -130,6 +131,14 @@ impl Configuration { .transpose()?, target_version: options.target_version, unfixable: options.unfixable, + cache_dir: options + .cache_dir + .map(|dir| { + let dir = shellexpand::full(&dir); + dir.map(|dir| PathBuf::from(dir.as_ref())) + }) + .transpose() + .map_err(|e| anyhow!("Invalid `cache-dir` value: {e}"))?, // Plugins flake8_annotations: options.flake8_annotations, flake8_bugbear: options.flake8_bugbear, @@ -184,6 +193,7 @@ impl Configuration { src: self.src.or(config.src), target_version: self.target_version.or(config.target_version), unfixable: self.unfixable.or(config.unfixable), + cache_dir: self.cache_dir.or(config.cache_dir), // Plugins flake8_annotations: self.flake8_annotations.or(config.flake8_annotations), flake8_bugbear: self.flake8_bugbear.or(config.flake8_bugbear), @@ -254,6 +264,9 @@ impl Configuration { if let Some(unfixable) = overrides.unfixable { self.unfixable = Some(unfixable); } + if let Some(cache_dir) = overrides.cache_dir { + self.cache_dir = Some(cache_dir); + } // Special-case: `extend_ignore` and `extend_select` are parallel arrays, so // push an empty array if only one of the two is provided. match (overrides.extend_ignore, overrides.extend_select) { diff --git a/src/settings/mod.rs b/src/settings/mod.rs index b5fb10847205b..de360546b7c4b 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -13,6 +13,7 @@ use path_absolutize::path_dedot; use regex::Regex; use rustc_hash::FxHashSet; +use crate::cache::cache_dir; use crate::checks::CheckCode; use crate::checks_gen::{CheckCodePrefix, SuffixLength, CATEGORIES}; use crate::settings::configuration::Configuration; @@ -49,6 +50,7 @@ pub struct Settings { pub show_source: bool, pub src: Vec, pub target_version: PythonVersion, + pub cache_dir: PathBuf, // Plugins pub flake8_annotations: flake8_annotations::settings::Settings, pub flake8_bugbear: flake8_bugbear::settings::Settings, @@ -140,6 +142,7 @@ impl Settings { .unwrap_or_else(|| vec![project_root.to_path_buf()]), target_version: config.target_version.unwrap_or(PythonVersion::Py310), show_source: config.show_source.unwrap_or_default(), + cache_dir: config.cache_dir.unwrap_or_else(|| cache_dir(project_root)), // Plugins flake8_annotations: config .flake8_annotations @@ -209,6 +212,7 @@ impl Settings { show_source: false, src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, + cache_dir: cache_dir(path_dedot::CWD.as_path()), flake8_annotations: flake8_annotations::settings::Settings::default(), flake8_bugbear: flake8_bugbear::settings::Settings::default(), flake8_errmsg: flake8_errmsg::settings::Settings::default(), @@ -242,6 +246,7 @@ impl Settings { show_source: false, src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, + cache_dir: cache_dir(path_dedot::CWD.as_path()), flake8_annotations: flake8_annotations::settings::Settings::default(), flake8_bugbear: flake8_bugbear::settings::Settings::default(), flake8_errmsg: flake8_errmsg::settings::Settings::default(), diff --git a/src/settings/options.rs b/src/settings/options.rs index e29f609c3d9b9..ffb11818f97bc 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -321,6 +321,23 @@ pub struct Options { "# )] pub unfixable: Option>, + #[option( + doc = r#" + A path to the cache directory. + + By default, Ruff stores cache results in a `.ruff_cache` directory in the current + project root. + + However, Ruff will also respect the `RUFF_CACHE_DIR` environment variable, which takes + precedence over that default. + + This setting will override even the `RUFF_CACHE_DIR` environment variable, if set. + "#, + default = ".ruff_cache", + value_type = "PathBuf", + example = r#"cache-dir = "~/.cache/ruff""# + )] + pub cache_dir: Option, // Plugins #[option_group] pub flake8_annotations: Option, diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index e7719077ec199..e7321ee9b530d 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -141,6 +141,7 @@ mod tests { src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -189,6 +190,7 @@ line-length = 79 src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -237,6 +239,7 @@ exclude = ["foo.py"] src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_errmsg: None, flake8_bugbear: None, @@ -285,6 +288,7 @@ select = ["E501"] src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -334,6 +338,7 @@ ignore = ["E501"] src: None, target_version: None, unfixable: None, + cache_dir: None, flake8_annotations: None, flake8_bugbear: None, flake8_errmsg: None, @@ -415,6 +420,7 @@ other-attribute = 1 format: None, force_exclude: None, unfixable: None, + cache_dir: None, per_file_ignores: Some(FxHashMap::from_iter([( "__init__.py".to_string(), vec![CheckCodePrefix::F401]