Files
ruff/crates/ruff/src/resolve.rs
2025-05-16 13:25:28 +02:00

146 lines
5.7 KiB
Rust

use std::path::Path;
use anyhow::{Result, bail};
use log::debug;
use path_absolutize::path_dedot;
use ruff_workspace::configuration::Configuration;
use ruff_workspace::pyproject::{self, find_fallback_target_version};
use ruff_workspace::resolver::{
ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy,
resolve_root_settings,
};
use ruff_python_ast as ast;
use crate::args::ConfigArguments;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
pub fn resolve(
config_arguments: &ConfigArguments,
stdin_filename: Option<&Path>,
) -> Result<PyprojectConfig> {
let Ok(cwd) = std::env::current_dir() else {
bail!("Working directory does not exist")
};
// First priority: if we're running in isolated mode, use the default settings.
if config_arguments.isolated {
let config = config_arguments.transform(Configuration::default());
let settings = config.into_settings(&cwd)?;
debug!("Isolated mode, not reading any pyproject.toml");
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed,
settings,
None,
));
}
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
if let Some(pyproject) = config_arguments.config_file() {
let settings = resolve_root_settings(
pyproject,
config_arguments,
ConfigurationOrigin::UserSpecified,
)?;
debug!(
"Using user-specified configuration file at: {}",
pyproject.display()
);
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed,
settings,
Some(pyproject.to_path_buf()),
));
}
// Third priority: find a `pyproject.toml` file in either an ancestor of
// `stdin_filename` (if set) or the current working path all paths relative to
// that directory. (With `Strategy::Hierarchical`, we'll end up finding
// the "closest" `pyproject.toml` file for every Python file later on,
// so these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_settings_toml(stdin_filename.unwrap_or(&cwd))? {
debug!(
"Using configuration file (via parent) at: {}",
pyproject.display()
);
let settings =
resolve_root_settings(&pyproject, config_arguments, ConfigurationOrigin::Ancestor)?;
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,
settings,
Some(pyproject),
));
}
// Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_user_settings_toml() {
struct FallbackTransformer<'a> {
arguments: &'a ConfigArguments,
}
impl ConfigurationTransformer for FallbackTransformer<'_> {
fn transform(&self, mut configuration: Configuration) -> Configuration {
// The `requires-python` constraint from the `pyproject.toml` takes precedence
// over the `target-version` from the user configuration.
let fallback = find_fallback_target_version(&*path_dedot::CWD);
if let Some(fallback) = fallback {
debug!("Derived `target-version` from found `requires-python`: {fallback:?}");
configuration.target_version = Some(fallback.into());
}
self.arguments.transform(configuration)
}
}
debug!(
"Using configuration file (via cwd) at: {}",
pyproject.display()
);
let settings = resolve_root_settings(
&pyproject,
&FallbackTransformer {
arguments: config_arguments,
},
ConfigurationOrigin::UserSettings,
)?;
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,
settings,
Some(pyproject),
));
}
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
debug!("Using Ruff default settings");
let mut config = config_arguments.transform(Configuration::default());
if config.target_version.is_none() {
// If we have arrived here we know that there was no `pyproject.toml`
// containing a `[tool.ruff]` section found in an ancestral directory.
// (This is an implicit requirement in the function
// `pyproject::find_settings_toml`.)
// However, there may be a `pyproject.toml` with a `requires-python`
// specified, and that is what we look for in this step.
let fallback = find_fallback_target_version(stdin_filename.unwrap_or(&cwd));
if let Some(version) = fallback {
debug!("Derived `target-version` from found `requires-python`: {version:?}");
}
config.target_version = fallback.map(ast::PythonVersion::from);
}
let settings = config.into_settings(&cwd)?;
Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical,
settings,
None,
))
}