mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 21:05:08 +00:00
Move Configuration
to ruff_workspace
crate (#6920)
This commit is contained in:
parent
039694aaed
commit
a6aa16630d
77 changed files with 3704 additions and 4108 deletions
906
crates/ruff_workspace/src/configuration.rs
Normal file
906
crates/ruff_workspace/src/configuration.rs
Normal file
|
@ -0,0 +1,906 @@
|
|||
//! User-provided program settings, taking into account pyproject.toml and
|
||||
//! command-line options. Structure mirrors the user-facing representation of
|
||||
//! the various parameters.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::env::VarError;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::options::{
|
||||
Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BugbearOptions, Flake8BuiltinsOptions,
|
||||
Flake8ComprehensionsOptions, Flake8CopyrightOptions, Flake8ErrMsgOptions, Flake8GetTextOptions,
|
||||
Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions,
|
||||
Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
|
||||
Flake8UnusedArgumentsOptions, IsortOptions, McCabeOptions, Options, Pep8NamingOptions,
|
||||
PyUpgradeOptions, PycodestyleOptions, PydocstyleOptions, PyflakesOptions, PylintOptions,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use glob::{glob, GlobError, Paths, PatternError};
|
||||
use regex::Regex;
|
||||
use ruff::line_width::{LineLength, TabSize};
|
||||
use ruff::registry::RuleNamespace;
|
||||
use ruff::registry::{Rule, RuleSet, INCOMPATIBLE_CODES};
|
||||
use ruff::rule_selector::Specificity;
|
||||
use ruff::settings::rule_table::RuleTable;
|
||||
use ruff::settings::types::{
|
||||
FilePattern, FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat, Version,
|
||||
};
|
||||
use ruff::settings::{defaults, resolve_per_file_ignores, AllSettings, CliSettings, Settings};
|
||||
use ruff::{fs, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION};
|
||||
use ruff_cache::cache_dir;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use shellexpand;
|
||||
use shellexpand::LookupError;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RuleSelection {
|
||||
pub select: Option<Vec<RuleSelector>>,
|
||||
pub ignore: Vec<RuleSelector>,
|
||||
pub extend_select: Vec<RuleSelector>,
|
||||
pub fixable: Option<Vec<RuleSelector>>,
|
||||
pub unfixable: Vec<RuleSelector>,
|
||||
pub extend_fixable: Vec<RuleSelector>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Configuration {
|
||||
pub rule_selections: Vec<RuleSelection>,
|
||||
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
|
||||
|
||||
pub allowed_confusables: Option<Vec<char>>,
|
||||
pub builtins: Option<Vec<String>>,
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
pub dummy_variable_rgx: Option<Regex>,
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
pub extend: Option<PathBuf>,
|
||||
pub extend_exclude: Vec<FilePattern>,
|
||||
pub extend_include: Vec<FilePattern>,
|
||||
pub extend_per_file_ignores: Vec<PerFileIgnore>,
|
||||
pub external: Option<Vec<String>>,
|
||||
pub fix: Option<bool>,
|
||||
pub fix_only: Option<bool>,
|
||||
pub force_exclude: Option<bool>,
|
||||
pub format: Option<SerializationFormat>,
|
||||
pub ignore_init_module_imports: Option<bool>,
|
||||
pub include: Option<Vec<FilePattern>>,
|
||||
pub line_length: Option<LineLength>,
|
||||
pub logger_objects: Option<Vec<String>>,
|
||||
pub namespace_packages: Option<Vec<PathBuf>>,
|
||||
pub required_version: Option<Version>,
|
||||
pub respect_gitignore: Option<bool>,
|
||||
pub show_fixes: Option<bool>,
|
||||
pub show_source: Option<bool>,
|
||||
pub src: Option<Vec<PathBuf>>,
|
||||
pub tab_size: Option<TabSize>,
|
||||
pub target_version: Option<PythonVersion>,
|
||||
pub task_tags: Option<Vec<String>>,
|
||||
pub typing_modules: Option<Vec<String>>,
|
||||
// Plugins
|
||||
pub flake8_annotations: Option<Flake8AnnotationsOptions>,
|
||||
pub flake8_bandit: Option<Flake8BanditOptions>,
|
||||
pub flake8_bugbear: Option<Flake8BugbearOptions>,
|
||||
pub flake8_builtins: Option<Flake8BuiltinsOptions>,
|
||||
pub flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
|
||||
pub flake8_copyright: Option<Flake8CopyrightOptions>,
|
||||
pub flake8_errmsg: Option<Flake8ErrMsgOptions>,
|
||||
pub flake8_gettext: Option<Flake8GetTextOptions>,
|
||||
pub flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,
|
||||
pub flake8_import_conventions: Option<Flake8ImportConventionsOptions>,
|
||||
pub flake8_pytest_style: Option<Flake8PytestStyleOptions>,
|
||||
pub flake8_quotes: Option<Flake8QuotesOptions>,
|
||||
pub flake8_self: Option<Flake8SelfOptions>,
|
||||
pub flake8_tidy_imports: Option<Flake8TidyImportsOptions>,
|
||||
pub flake8_type_checking: Option<Flake8TypeCheckingOptions>,
|
||||
pub flake8_unused_arguments: Option<Flake8UnusedArgumentsOptions>,
|
||||
pub isort: Option<IsortOptions>,
|
||||
pub mccabe: Option<McCabeOptions>,
|
||||
pub pep8_naming: Option<Pep8NamingOptions>,
|
||||
pub pycodestyle: Option<PycodestyleOptions>,
|
||||
pub pydocstyle: Option<PydocstyleOptions>,
|
||||
pub pyflakes: Option<PyflakesOptions>,
|
||||
pub pylint: Option<PylintOptions>,
|
||||
pub pyupgrade: Option<PyUpgradeOptions>,
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn into_all_settings(self, project_root: &Path) -> Result<AllSettings> {
|
||||
Ok(AllSettings {
|
||||
cli: CliSettings {
|
||||
cache_dir: self
|
||||
.cache_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| cache_dir(project_root)),
|
||||
fix: self.fix.unwrap_or(false),
|
||||
fix_only: self.fix_only.unwrap_or(false),
|
||||
format: self.format.unwrap_or_default(),
|
||||
show_fixes: self.show_fixes.unwrap_or(false),
|
||||
show_source: self.show_source.unwrap_or(false),
|
||||
},
|
||||
lib: self.into_settings(project_root)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_settings(self, project_root: &Path) -> Result<Settings> {
|
||||
if let Some(required_version) = &self.required_version {
|
||||
if &**required_version != RUFF_PKG_VERSION {
|
||||
return Err(anyhow!(
|
||||
"Required version `{}` does not match the running version `{}`",
|
||||
&**required_version,
|
||||
RUFF_PKG_VERSION
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Settings {
|
||||
rules: self.as_rule_table(),
|
||||
allowed_confusables: self
|
||||
.allowed_confusables
|
||||
.map(FxHashSet::from_iter)
|
||||
.unwrap_or_default(),
|
||||
builtins: self.builtins.unwrap_or_default(),
|
||||
dummy_variable_rgx: self
|
||||
.dummy_variable_rgx
|
||||
.unwrap_or_else(|| defaults::DUMMY_VARIABLE_RGX.clone()),
|
||||
exclude: FilePatternSet::try_from_vec(
|
||||
self.exclude.unwrap_or_else(|| defaults::EXCLUDE.clone()),
|
||||
)?,
|
||||
extend_exclude: FilePatternSet::try_from_vec(self.extend_exclude)?,
|
||||
extend_include: FilePatternSet::try_from_vec(self.extend_include)?,
|
||||
external: FxHashSet::from_iter(self.external.unwrap_or_default()),
|
||||
force_exclude: self.force_exclude.unwrap_or(false),
|
||||
include: FilePatternSet::try_from_vec(
|
||||
self.include.unwrap_or_else(|| defaults::INCLUDE.clone()),
|
||||
)?,
|
||||
ignore_init_module_imports: self.ignore_init_module_imports.unwrap_or_default(),
|
||||
line_length: self.line_length.unwrap_or_default(),
|
||||
tab_size: self.tab_size.unwrap_or_default(),
|
||||
namespace_packages: self.namespace_packages.unwrap_or_default(),
|
||||
per_file_ignores: resolve_per_file_ignores(
|
||||
self.per_file_ignores
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(self.extend_per_file_ignores)
|
||||
.collect(),
|
||||
)?,
|
||||
respect_gitignore: self.respect_gitignore.unwrap_or(true),
|
||||
src: self.src.unwrap_or_else(|| vec![project_root.to_path_buf()]),
|
||||
project_root: project_root.to_path_buf(),
|
||||
target_version: self.target_version.unwrap_or_default(),
|
||||
task_tags: self.task_tags.unwrap_or_else(|| {
|
||||
defaults::TASK_TAGS
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}),
|
||||
logger_objects: self.logger_objects.unwrap_or_default(),
|
||||
typing_modules: self.typing_modules.unwrap_or_default(),
|
||||
// Plugins
|
||||
flake8_annotations: self
|
||||
.flake8_annotations
|
||||
.map(Flake8AnnotationsOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_bandit: self
|
||||
.flake8_bandit
|
||||
.map(Flake8BanditOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_bugbear: self
|
||||
.flake8_bugbear
|
||||
.map(Flake8BugbearOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_builtins: self
|
||||
.flake8_builtins
|
||||
.map(Flake8BuiltinsOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_comprehensions: self
|
||||
.flake8_comprehensions
|
||||
.map(Flake8ComprehensionsOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_copyright: self
|
||||
.flake8_copyright
|
||||
.map(Flake8CopyrightOptions::try_into_settings)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
flake8_errmsg: self
|
||||
.flake8_errmsg
|
||||
.map(Flake8ErrMsgOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_implicit_str_concat: self
|
||||
.flake8_implicit_str_concat
|
||||
.map(Flake8ImplicitStrConcatOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_import_conventions: self
|
||||
.flake8_import_conventions
|
||||
.map(Flake8ImportConventionsOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_pytest_style: self
|
||||
.flake8_pytest_style
|
||||
.map(Flake8PytestStyleOptions::try_into_settings)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
flake8_quotes: self
|
||||
.flake8_quotes
|
||||
.map(Flake8QuotesOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_self: self
|
||||
.flake8_self
|
||||
.map(Flake8SelfOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_tidy_imports: self
|
||||
.flake8_tidy_imports
|
||||
.map(Flake8TidyImportsOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_type_checking: self
|
||||
.flake8_type_checking
|
||||
.map(Flake8TypeCheckingOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_unused_arguments: self
|
||||
.flake8_unused_arguments
|
||||
.map(Flake8UnusedArgumentsOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
flake8_gettext: self
|
||||
.flake8_gettext
|
||||
.map(Flake8GetTextOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
isort: self
|
||||
.isort
|
||||
.map(IsortOptions::try_into_settings)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
mccabe: self
|
||||
.mccabe
|
||||
.map(McCabeOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
pep8_naming: self
|
||||
.pep8_naming
|
||||
.map(Pep8NamingOptions::try_into_settings)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
pycodestyle: self
|
||||
.pycodestyle
|
||||
.map(PycodestyleOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
pydocstyle: self
|
||||
.pydocstyle
|
||||
.map(PydocstyleOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
pyflakes: self
|
||||
.pyflakes
|
||||
.map(PyflakesOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
pylint: self
|
||||
.pylint
|
||||
.map(PylintOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
pyupgrade: self
|
||||
.pyupgrade
|
||||
.map(PyUpgradeOptions::into_settings)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {
|
||||
Ok(Self {
|
||||
rule_selections: vec![RuleSelection {
|
||||
select: options.select,
|
||||
ignore: options
|
||||
.ignore
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(options.extend_ignore.into_iter().flatten())
|
||||
.collect(),
|
||||
extend_select: options.extend_select.unwrap_or_default(),
|
||||
fixable: options.fixable,
|
||||
unfixable: options
|
||||
.unfixable
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(options.extend_unfixable.into_iter().flatten())
|
||||
.collect(),
|
||||
extend_fixable: options.extend_fixable.unwrap_or_default(),
|
||||
}],
|
||||
allowed_confusables: options.allowed_confusables,
|
||||
builtins: options.builtins,
|
||||
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}"))?,
|
||||
dummy_variable_rgx: options
|
||||
.dummy_variable_rgx
|
||||
.map(|pattern| Regex::new(&pattern))
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
|
||||
exclude: options.exclude.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|pattern| {
|
||||
let absolute = fs::normalize_path_to(&pattern, project_root);
|
||||
FilePattern::User(pattern, absolute)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
extend: options
|
||||
.extend
|
||||
.map(|extend| {
|
||||
let extend = shellexpand::full(&extend);
|
||||
extend.map(|extend| PathBuf::from(extend.as_ref()))
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!("Invalid `extend` value: {e}"))?,
|
||||
extend_exclude: options
|
||||
.extend_exclude
|
||||
.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|pattern| {
|
||||
let absolute = fs::normalize_path_to(&pattern, project_root);
|
||||
FilePattern::User(pattern, absolute)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
extend_include: options
|
||||
.extend_include
|
||||
.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|pattern| {
|
||||
let absolute = fs::normalize_path_to(&pattern, project_root);
|
||||
FilePattern::User(pattern, absolute)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
extend_per_file_ignores: options
|
||||
.extend_per_file_ignores
|
||||
.map(|per_file_ignores| {
|
||||
per_file_ignores
|
||||
.into_iter()
|
||||
.map(|(pattern, prefixes)| {
|
||||
PerFileIgnore::new(pattern, &prefixes, Some(project_root))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
external: options.external,
|
||||
fix: options.fix,
|
||||
fix_only: options.fix_only,
|
||||
format: options.format,
|
||||
force_exclude: options.force_exclude,
|
||||
ignore_init_module_imports: options.ignore_init_module_imports,
|
||||
include: options.include.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|pattern| {
|
||||
let absolute = fs::normalize_path_to(&pattern, project_root);
|
||||
FilePattern::User(pattern, absolute)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
line_length: options.line_length,
|
||||
tab_size: options.tab_size,
|
||||
namespace_packages: options
|
||||
.namespace_packages
|
||||
.map(|namespace_package| resolve_src(&namespace_package, project_root))
|
||||
.transpose()?,
|
||||
per_file_ignores: options.per_file_ignores.map(|per_file_ignores| {
|
||||
per_file_ignores
|
||||
.into_iter()
|
||||
.map(|(pattern, prefixes)| {
|
||||
PerFileIgnore::new(pattern, &prefixes, Some(project_root))
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
required_version: options.required_version,
|
||||
respect_gitignore: options.respect_gitignore,
|
||||
show_source: options.show_source,
|
||||
show_fixes: options.show_fixes,
|
||||
src: options
|
||||
.src
|
||||
.map(|src| resolve_src(&src, project_root))
|
||||
.transpose()?,
|
||||
target_version: options.target_version,
|
||||
task_tags: options.task_tags,
|
||||
logger_objects: options.logger_objects,
|
||||
typing_modules: options.typing_modules,
|
||||
// Plugins
|
||||
flake8_annotations: options.flake8_annotations,
|
||||
flake8_bandit: options.flake8_bandit,
|
||||
flake8_bugbear: options.flake8_bugbear,
|
||||
flake8_builtins: options.flake8_builtins,
|
||||
flake8_comprehensions: options.flake8_comprehensions,
|
||||
flake8_copyright: options.flake8_copyright,
|
||||
flake8_errmsg: options.flake8_errmsg,
|
||||
flake8_gettext: options.flake8_gettext,
|
||||
flake8_implicit_str_concat: options.flake8_implicit_str_concat,
|
||||
flake8_import_conventions: options.flake8_import_conventions,
|
||||
flake8_pytest_style: options.flake8_pytest_style,
|
||||
flake8_quotes: options.flake8_quotes,
|
||||
flake8_self: options.flake8_self,
|
||||
flake8_tidy_imports: options.flake8_tidy_imports,
|
||||
flake8_type_checking: options.flake8_type_checking,
|
||||
flake8_unused_arguments: options.flake8_unused_arguments,
|
||||
isort: options.isort,
|
||||
mccabe: options.mccabe,
|
||||
pep8_naming: options.pep8_naming,
|
||||
pycodestyle: options.pycodestyle,
|
||||
pydocstyle: options.pydocstyle,
|
||||
pyflakes: options.pyflakes,
|
||||
pylint: options.pylint,
|
||||
pyupgrade: options.pyupgrade,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_rule_table(&self) -> RuleTable {
|
||||
// The select_set keeps track of which rules have been selected.
|
||||
let mut select_set: RuleSet = defaults::PREFIXES.iter().flatten().collect();
|
||||
// The fixable set keeps track of which rules are fixable.
|
||||
let mut fixable_set: RuleSet = RuleSelector::All
|
||||
.into_iter()
|
||||
.chain(&RuleSelector::Nursery)
|
||||
.collect();
|
||||
|
||||
// Ignores normally only subtract from the current set of selected
|
||||
// rules. By that logic the ignore in `select = [], ignore = ["E501"]`
|
||||
// would be effectless. Instead we carry over the ignores to the next
|
||||
// selection in that case, creating a way for ignores to be reused
|
||||
// across config files (which otherwise wouldn't be possible since ruff
|
||||
// only has `extended` but no `extended-by`).
|
||||
let mut carryover_ignores: Option<&[RuleSelector]> = None;
|
||||
let mut carryover_unfixables: Option<&[RuleSelector]> = None;
|
||||
|
||||
let mut redirects = FxHashMap::default();
|
||||
|
||||
for selection in &self.rule_selections {
|
||||
// If a selection only specifies extend-select we cannot directly
|
||||
// apply its rule selectors to the select_set because we firstly have
|
||||
// to resolve the effectively selected rules within the current rule selection
|
||||
// (taking specificity into account since more specific selectors take
|
||||
// precedence over less specific selectors within a rule selection).
|
||||
// We do this via the following HashMap where the bool indicates
|
||||
// whether to enable or disable the given rule.
|
||||
let mut select_map_updates: FxHashMap<Rule, bool> = FxHashMap::default();
|
||||
let mut fixable_map_updates: FxHashMap<Rule, bool> = FxHashMap::default();
|
||||
|
||||
let carriedover_ignores = carryover_ignores.take();
|
||||
let carriedover_unfixables = carryover_unfixables.take();
|
||||
|
||||
for spec in Specificity::iter() {
|
||||
// Iterate over rule selectors in order of specificity.
|
||||
for selector in selection
|
||||
.select
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(selection.extend_select.iter())
|
||||
.filter(|s| s.specificity() == spec)
|
||||
{
|
||||
for rule in selector {
|
||||
select_map_updates.insert(rule, true);
|
||||
}
|
||||
}
|
||||
for selector in selection
|
||||
.ignore
|
||||
.iter()
|
||||
.chain(carriedover_ignores.into_iter().flatten())
|
||||
.filter(|s| s.specificity() == spec)
|
||||
{
|
||||
for rule in selector {
|
||||
select_map_updates.insert(rule, false);
|
||||
}
|
||||
}
|
||||
// Apply the same logic to `fixable` and `unfixable`.
|
||||
for selector in selection
|
||||
.fixable
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(selection.extend_fixable.iter())
|
||||
.filter(|s| s.specificity() == spec)
|
||||
{
|
||||
for rule in selector {
|
||||
fixable_map_updates.insert(rule, true);
|
||||
}
|
||||
}
|
||||
for selector in selection
|
||||
.unfixable
|
||||
.iter()
|
||||
.chain(carriedover_unfixables.into_iter().flatten())
|
||||
.filter(|s| s.specificity() == spec)
|
||||
{
|
||||
for rule in selector {
|
||||
fixable_map_updates.insert(rule, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(select) = &selection.select {
|
||||
// If the `select` option is given we reassign the whole select_set
|
||||
// (overriding everything that has been defined previously).
|
||||
select_set = select_map_updates
|
||||
.into_iter()
|
||||
.filter_map(|(rule, enabled)| enabled.then_some(rule))
|
||||
.collect();
|
||||
|
||||
if select.is_empty()
|
||||
&& selection.extend_select.is_empty()
|
||||
&& !selection.ignore.is_empty()
|
||||
{
|
||||
carryover_ignores = Some(&selection.ignore);
|
||||
}
|
||||
} else {
|
||||
// Otherwise we apply the updates on top of the existing select_set.
|
||||
for (rule, enabled) in select_map_updates {
|
||||
if enabled {
|
||||
select_set.insert(rule);
|
||||
} else {
|
||||
select_set.remove(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the same logic to `fixable` and `unfixable`.
|
||||
if let Some(fixable) = &selection.fixable {
|
||||
fixable_set = fixable_map_updates
|
||||
.into_iter()
|
||||
.filter_map(|(rule, enabled)| enabled.then_some(rule))
|
||||
.collect();
|
||||
|
||||
if fixable.is_empty()
|
||||
&& selection.extend_fixable.is_empty()
|
||||
&& !selection.unfixable.is_empty()
|
||||
{
|
||||
carryover_unfixables = Some(&selection.unfixable);
|
||||
}
|
||||
} else {
|
||||
for (rule, enabled) in fixable_map_updates {
|
||||
if enabled {
|
||||
fixable_set.insert(rule);
|
||||
} else {
|
||||
fixable_set.remove(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We insert redirects into the hashmap so that we
|
||||
// can warn the users about remapped rule codes.
|
||||
for selector in selection
|
||||
.select
|
||||
.iter()
|
||||
.chain(selection.fixable.iter())
|
||||
.flatten()
|
||||
.chain(selection.ignore.iter())
|
||||
.chain(selection.extend_select.iter())
|
||||
.chain(selection.unfixable.iter())
|
||||
.chain(selection.extend_fixable.iter())
|
||||
{
|
||||
if let RuleSelector::Prefix {
|
||||
prefix,
|
||||
redirected_from: Some(redirect_from),
|
||||
} = selector
|
||||
{
|
||||
redirects.insert(redirect_from, prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (from, target) in redirects {
|
||||
// TODO(martin): This belongs into the ruff_cli crate.
|
||||
warn_user_once_by_id!(
|
||||
from,
|
||||
"`{from}` has been remapped to `{}{}`.",
|
||||
target.linter().common_prefix(),
|
||||
target.short_code()
|
||||
);
|
||||
}
|
||||
|
||||
let mut rules = RuleTable::empty();
|
||||
|
||||
for rule in select_set {
|
||||
let fix = fixable_set.contains(rule);
|
||||
rules.enable(rule, fix);
|
||||
}
|
||||
|
||||
// If a docstring convention is specified, force-disable any incompatible error
|
||||
// codes.
|
||||
if let Some(convention) = self
|
||||
.pydocstyle
|
||||
.as_ref()
|
||||
.and_then(|pydocstyle| pydocstyle.convention)
|
||||
{
|
||||
for rule in convention.rules_to_be_ignored() {
|
||||
rules.disable(*rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that we didn't enable any incompatible rules. Use this awkward
|
||||
// approach to give each pair it's own `warn_user_once`.
|
||||
for (preferred, expendable, message) in INCOMPATIBLE_CODES {
|
||||
if rules.enabled(*preferred) && rules.enabled(*expendable) {
|
||||
warn_user_once_by_id!(expendable.as_ref(), "{}", message);
|
||||
rules.disable(*expendable);
|
||||
}
|
||||
}
|
||||
|
||||
rules
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn combine(self, config: Self) -> Self {
|
||||
Self {
|
||||
rule_selections: config
|
||||
.rule_selections
|
||||
.into_iter()
|
||||
.chain(self.rule_selections)
|
||||
.collect(),
|
||||
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
|
||||
builtins: self.builtins.or(config.builtins),
|
||||
cache_dir: self.cache_dir.or(config.cache_dir),
|
||||
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
|
||||
exclude: self.exclude.or(config.exclude),
|
||||
extend: self.extend.or(config.extend),
|
||||
extend_exclude: config
|
||||
.extend_exclude
|
||||
.into_iter()
|
||||
.chain(self.extend_exclude)
|
||||
.collect(),
|
||||
extend_include: config
|
||||
.extend_include
|
||||
.into_iter()
|
||||
.chain(self.extend_include)
|
||||
.collect(),
|
||||
extend_per_file_ignores: config
|
||||
.extend_per_file_ignores
|
||||
.into_iter()
|
||||
.chain(self.extend_per_file_ignores)
|
||||
.collect(),
|
||||
external: self.external.or(config.external),
|
||||
fix: self.fix.or(config.fix),
|
||||
fix_only: self.fix_only.or(config.fix_only),
|
||||
format: self.format.or(config.format),
|
||||
force_exclude: self.force_exclude.or(config.force_exclude),
|
||||
include: self.include.or(config.include),
|
||||
ignore_init_module_imports: self
|
||||
.ignore_init_module_imports
|
||||
.or(config.ignore_init_module_imports),
|
||||
line_length: self.line_length.or(config.line_length),
|
||||
logger_objects: self.logger_objects.or(config.logger_objects),
|
||||
tab_size: self.tab_size.or(config.tab_size),
|
||||
namespace_packages: self.namespace_packages.or(config.namespace_packages),
|
||||
per_file_ignores: self.per_file_ignores.or(config.per_file_ignores),
|
||||
required_version: self.required_version.or(config.required_version),
|
||||
respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
|
||||
show_source: self.show_source.or(config.show_source),
|
||||
show_fixes: self.show_fixes.or(config.show_fixes),
|
||||
src: self.src.or(config.src),
|
||||
target_version: self.target_version.or(config.target_version),
|
||||
task_tags: self.task_tags.or(config.task_tags),
|
||||
typing_modules: self.typing_modules.or(config.typing_modules),
|
||||
// Plugins
|
||||
flake8_annotations: self.flake8_annotations.combine(config.flake8_annotations),
|
||||
flake8_bandit: self.flake8_bandit.combine(config.flake8_bandit),
|
||||
flake8_bugbear: self.flake8_bugbear.combine(config.flake8_bugbear),
|
||||
flake8_builtins: self.flake8_builtins.combine(config.flake8_builtins),
|
||||
flake8_comprehensions: self
|
||||
.flake8_comprehensions
|
||||
.combine(config.flake8_comprehensions),
|
||||
flake8_copyright: self.flake8_copyright.combine(config.flake8_copyright),
|
||||
flake8_errmsg: self.flake8_errmsg.combine(config.flake8_errmsg),
|
||||
flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext),
|
||||
flake8_implicit_str_concat: self
|
||||
.flake8_implicit_str_concat
|
||||
.combine(config.flake8_implicit_str_concat),
|
||||
flake8_import_conventions: self
|
||||
.flake8_import_conventions
|
||||
.combine(config.flake8_import_conventions),
|
||||
flake8_pytest_style: self.flake8_pytest_style.combine(config.flake8_pytest_style),
|
||||
flake8_quotes: self.flake8_quotes.combine(config.flake8_quotes),
|
||||
flake8_self: self.flake8_self.combine(config.flake8_self),
|
||||
flake8_tidy_imports: self.flake8_tidy_imports.combine(config.flake8_tidy_imports),
|
||||
flake8_type_checking: self
|
||||
.flake8_type_checking
|
||||
.combine(config.flake8_type_checking),
|
||||
flake8_unused_arguments: self
|
||||
.flake8_unused_arguments
|
||||
.combine(config.flake8_unused_arguments),
|
||||
isort: self.isort.combine(config.isort),
|
||||
mccabe: self.mccabe.combine(config.mccabe),
|
||||
pep8_naming: self.pep8_naming.combine(config.pep8_naming),
|
||||
pycodestyle: self.pycodestyle.combine(config.pycodestyle),
|
||||
pydocstyle: self.pydocstyle.combine(config.pydocstyle),
|
||||
pyflakes: self.pyflakes.combine(config.pyflakes),
|
||||
pylint: self.pylint.combine(config.pylint),
|
||||
pyupgrade: self.pyupgrade.combine(config.pyupgrade),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait CombinePluginOptions {
|
||||
#[must_use]
|
||||
fn combine(self, other: Self) -> Self;
|
||||
}
|
||||
|
||||
impl<T: CombinePluginOptions> CombinePluginOptions for Option<T> {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
match (self, other) {
|
||||
(Some(base), Some(other)) => Some(base.combine(other)),
|
||||
(Some(base), None) => Some(base),
|
||||
(None, Some(other)) => Some(other),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a list of source paths, which could include glob patterns, resolve the
|
||||
/// matching paths.
|
||||
pub fn resolve_src(src: &[String], project_root: &Path) -> Result<Vec<PathBuf>> {
|
||||
let expansions = src
|
||||
.iter()
|
||||
.map(shellexpand::full)
|
||||
.collect::<Result<Vec<Cow<'_, str>>, LookupError<VarError>>>()?;
|
||||
let globs = expansions
|
||||
.iter()
|
||||
.map(|path| Path::new(path.as_ref()))
|
||||
.map(|path| fs::normalize_path_to(path, project_root))
|
||||
.map(|path| glob(&path.to_string_lossy()))
|
||||
.collect::<Result<Vec<Paths>, PatternError>>()?;
|
||||
let paths: Vec<PathBuf> = globs
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Result<Vec<PathBuf>, GlobError>>()?;
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::configuration::{Configuration, RuleSelection};
|
||||
use ruff::codes::Pycodestyle;
|
||||
use ruff::registry::{Rule, RuleSet};
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn resolve_rules(selections: impl IntoIterator<Item = RuleSelection>) -> RuleSet {
|
||||
Configuration {
|
||||
rule_selections: selections.into_iter().collect(),
|
||||
..Configuration::default()
|
||||
}
|
||||
.as_rule_table()
|
||||
.iter_enabled()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_codes() {
|
||||
let actual = resolve_rules([RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W.into()]),
|
||||
..RuleSelection::default()
|
||||
}]);
|
||||
|
||||
let expected = RuleSet::from_rules(&[
|
||||
Rule::TrailingWhitespace,
|
||||
Rule::MissingNewlineAtEndOfFile,
|
||||
Rule::BlankLineWithWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::TabIndentation,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules([RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W6.into()]),
|
||||
..RuleSelection::default()
|
||||
}]);
|
||||
let expected = RuleSet::from_rule(Rule::InvalidEscapeSequence);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules([RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W.into()]),
|
||||
ignore: vec![Pycodestyle::W292.into()],
|
||||
..RuleSelection::default()
|
||||
}]);
|
||||
let expected = RuleSet::from_rules(&[
|
||||
Rule::TrailingWhitespace,
|
||||
Rule::BlankLineWithWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::TabIndentation,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules([RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W292.into()]),
|
||||
ignore: vec![Pycodestyle::W.into()],
|
||||
..RuleSelection::default()
|
||||
}]);
|
||||
let expected = RuleSet::from_rule(Rule::MissingNewlineAtEndOfFile);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules([RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W605.into()]),
|
||||
ignore: vec![Pycodestyle::W605.into()],
|
||||
..RuleSelection::default()
|
||||
}]);
|
||||
let expected = RuleSet::empty();
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules([
|
||||
RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W.into()]),
|
||||
ignore: vec![Pycodestyle::W292.into()],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
RuleSelection {
|
||||
extend_select: vec![Pycodestyle::W292.into()],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
]);
|
||||
let expected = RuleSet::from_rules(&[
|
||||
Rule::TrailingWhitespace,
|
||||
Rule::MissingNewlineAtEndOfFile,
|
||||
Rule::BlankLineWithWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::TabIndentation,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules([
|
||||
RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W.into()]),
|
||||
ignore: vec![Pycodestyle::W292.into()],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
RuleSelection {
|
||||
extend_select: vec![Pycodestyle::W292.into()],
|
||||
ignore: vec![Pycodestyle::W.into()],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
]);
|
||||
let expected = RuleSet::from_rule(Rule::MissingNewlineAtEndOfFile);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn carry_over_ignore() {
|
||||
let actual = resolve_rules([
|
||||
RuleSelection {
|
||||
select: Some(vec![]),
|
||||
ignore: vec![Pycodestyle::W292.into()],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W.into()]),
|
||||
..RuleSelection::default()
|
||||
},
|
||||
]);
|
||||
let expected = RuleSet::from_rules(&[
|
||||
Rule::TrailingWhitespace,
|
||||
Rule::BlankLineWithWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::TabIndentation,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules([
|
||||
RuleSelection {
|
||||
select: Some(vec![]),
|
||||
ignore: vec![Pycodestyle::W292.into()],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
RuleSelection {
|
||||
select: Some(vec![Pycodestyle::W.into()]),
|
||||
ignore: vec![Pycodestyle::W505.into()],
|
||||
..RuleSelection::default()
|
||||
},
|
||||
]);
|
||||
let expected = RuleSet::from_rules(&[
|
||||
Rule::TrailingWhitespace,
|
||||
Rule::BlankLineWithWhitespace,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::TabIndentation,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
15
crates/ruff_workspace/src/lib.rs
Normal file
15
crates/ruff_workspace/src/lib.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
pub mod configuration;
|
||||
pub mod options;
|
||||
pub mod pyproject;
|
||||
pub mod resolver;
|
||||
|
||||
pub mod options_base;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn test_resource_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
Path::new("../ruff/resources/test/").join(path)
|
||||
}
|
||||
}
|
2203
crates/ruff_workspace/src/options.rs
Normal file
2203
crates/ruff_workspace/src/options.rs
Normal file
File diff suppressed because it is too large
Load diff
180
crates/ruff_workspace/src/options_base.rs
Normal file
180
crates/ruff_workspace/src/options_base.rs
Normal file
|
@ -0,0 +1,180 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum OptionEntry {
|
||||
Field(OptionField),
|
||||
Group(OptionGroup),
|
||||
}
|
||||
|
||||
impl Display for OptionEntry {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
OptionEntry::Field(field) => field.fmt(f),
|
||||
OptionEntry::Group(group) => group.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct OptionGroup(&'static [(&'static str, OptionEntry)]);
|
||||
|
||||
impl OptionGroup {
|
||||
pub const fn new(options: &'static [(&'static str, OptionEntry)]) -> Self {
|
||||
Self(options)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> std::slice::Iter<(&str, OptionEntry)> {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
/// Get an option entry by its fully-qualified name
|
||||
/// (e.g. `foo.bar` refers to the `bar` option in the `foo` group).
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ### Find a direct child
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_workspace::options_base::{OptionGroup, OptionEntry, OptionField};
|
||||
///
|
||||
/// const options: [(&'static str, OptionEntry); 2] = [
|
||||
/// ("ignore_names", OptionEntry::Field(OptionField {
|
||||
/// doc: "ignore_doc",
|
||||
/// default: "ignore_default",
|
||||
/// value_type: "value_type",
|
||||
/// example: "ignore code"
|
||||
/// })),
|
||||
///
|
||||
/// ("global_names", OptionEntry::Field(OptionField {
|
||||
/// doc: "global_doc",
|
||||
/// default: "global_default",
|
||||
/// value_type: "value_type",
|
||||
/// example: "global code"
|
||||
/// }))
|
||||
/// ];
|
||||
///
|
||||
/// let group = OptionGroup::new(&options);
|
||||
///
|
||||
/// let ignore_names = group.get("ignore_names");
|
||||
///
|
||||
/// match ignore_names {
|
||||
/// None => panic!("Expect option 'ignore_names' to be Some"),
|
||||
/// Some(OptionEntry::Group(group)) => panic!("Expected 'ignore_names' to be a field but found group {}", group),
|
||||
/// Some(OptionEntry::Field(field)) => {
|
||||
/// assert_eq!("ignore_doc", field.doc);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(None, group.get("not_existing_option"));
|
||||
/// ```
|
||||
///
|
||||
/// ### Find a nested options
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ruff_workspace::options_base::{OptionGroup, OptionEntry, OptionField};
|
||||
///
|
||||
/// const ignore_options: [(&'static str, OptionEntry); 2] = [
|
||||
/// ("names", OptionEntry::Field(OptionField {
|
||||
/// doc: "ignore_name_doc",
|
||||
/// default: "ignore_name_default",
|
||||
/// value_type: "value_type",
|
||||
/// example: "ignore name code"
|
||||
/// })),
|
||||
///
|
||||
/// ("extensions", OptionEntry::Field(OptionField {
|
||||
/// doc: "ignore_extensions_doc",
|
||||
/// default: "ignore_extensions_default",
|
||||
/// value_type: "value_type",
|
||||
/// example: "ignore extensions code"
|
||||
/// }))
|
||||
/// ];
|
||||
///
|
||||
/// const options: [(&'static str, OptionEntry); 2] = [
|
||||
/// ("ignore", OptionEntry::Group(OptionGroup::new(&ignore_options))),
|
||||
///
|
||||
/// ("global_names", OptionEntry::Field(OptionField {
|
||||
/// doc: "global_doc",
|
||||
/// default: "global_default",
|
||||
/// value_type: "value_type",
|
||||
/// example: "global code"
|
||||
/// }))
|
||||
/// ];
|
||||
///
|
||||
/// let group = OptionGroup::new(&options);
|
||||
///
|
||||
/// let ignore_names = group.get("ignore.names");
|
||||
///
|
||||
/// match ignore_names {
|
||||
/// None => panic!("Expect option 'ignore.names' to be Some"),
|
||||
/// Some(OptionEntry::Group(group)) => panic!("Expected 'ignore_names' to be a field but found group {}", group),
|
||||
/// Some(OptionEntry::Field(field)) => {
|
||||
/// assert_eq!("ignore_name_doc", field.doc);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<&OptionEntry> {
|
||||
let mut parts = name.split('.').peekable();
|
||||
|
||||
let mut options = self.iter();
|
||||
|
||||
loop {
|
||||
let part = parts.next()?;
|
||||
|
||||
let (_, field) = options.find(|(name, _)| *name == part)?;
|
||||
|
||||
match (parts.peek(), field) {
|
||||
(None, field) => return Some(field),
|
||||
(Some(..), OptionEntry::Field(..)) => return None,
|
||||
(Some(..), OptionEntry::Group(group)) => {
|
||||
options = group.iter();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a OptionGroup {
|
||||
type IntoIter = std::slice::Iter<'a, (&'a str, OptionEntry)>;
|
||||
type Item = &'a (&'a str, OptionEntry);
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for OptionGroup {
|
||||
type IntoIter = std::slice::Iter<'static, (&'static str, OptionEntry)>;
|
||||
type Item = &'static (&'static str, OptionEntry);
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for OptionGroup {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
for (name, _) in self {
|
||||
writeln!(f, "{name}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct OptionField {
|
||||
pub doc: &'static str,
|
||||
pub default: &'static str,
|
||||
pub value_type: &'static str,
|
||||
pub example: &'static str,
|
||||
}
|
||||
|
||||
impl Display for OptionField {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "{}", self.doc)?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "Default value: {}", self.default)?;
|
||||
writeln!(f, "Type: {}", self.value_type)?;
|
||||
writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
|
||||
}
|
||||
}
|
338
crates/ruff_workspace/src/pyproject.rs
Normal file
338
crates/ruff_workspace/src/pyproject.rs
Normal file
|
@ -0,0 +1,338 @@
|
|||
//! Utilities for locating (and extracting configuration from) a pyproject.toml.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::options::Options;
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use pep440_rs::VersionSpecifiers;
|
||||
use ruff::settings::types::PythonVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct Tools {
|
||||
ruff: Option<Options>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
struct Project {
|
||||
#[serde(alias = "requires-python", alias = "requires_python")]
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct Pyproject {
|
||||
tool: Option<Tools>,
|
||||
project: Option<Project>,
|
||||
}
|
||||
|
||||
impl Pyproject {
|
||||
pub const fn new(options: Options) -> Self {
|
||||
Self {
|
||||
tool: Some(Tools {
|
||||
ruff: Some(options),
|
||||
}),
|
||||
project: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a `ruff.toml` file.
|
||||
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
toml::from_str(&contents).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Parse a `pyproject.toml` file.
|
||||
fn parse_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
toml::from_str(&contents).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
|
||||
pub fn ruff_enabled<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||
let pyproject = parse_pyproject_toml(path)?;
|
||||
Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some())
|
||||
}
|
||||
|
||||
/// Return the path to the `pyproject.toml` or `ruff.toml` file in a given
|
||||
/// directory.
|
||||
pub fn settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
|
||||
// Check for `.ruff.toml`.
|
||||
let ruff_toml = path.as_ref().join(".ruff.toml");
|
||||
if ruff_toml.is_file() {
|
||||
return Ok(Some(ruff_toml));
|
||||
}
|
||||
|
||||
// Check for `ruff.toml`.
|
||||
let ruff_toml = path.as_ref().join("ruff.toml");
|
||||
if ruff_toml.is_file() {
|
||||
return Ok(Some(ruff_toml));
|
||||
}
|
||||
|
||||
// Check for `pyproject.toml`.
|
||||
let pyproject_toml = path.as_ref().join("pyproject.toml");
|
||||
if pyproject_toml.is_file() && ruff_enabled(&pyproject_toml)? {
|
||||
return Ok(Some(pyproject_toml));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Find the path to the `pyproject.toml` or `ruff.toml` file, if such a file
|
||||
/// exists.
|
||||
pub fn find_settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
|
||||
for directory in path.as_ref().ancestors() {
|
||||
if let Some(pyproject) = settings_toml(directory)? {
|
||||
return Ok(Some(pyproject));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it
|
||||
/// exists.
|
||||
pub fn find_user_settings_toml() -> Option<PathBuf> {
|
||||
// Search for a user-specific `.ruff.toml`.
|
||||
let mut path = dirs::config_dir()?;
|
||||
path.push("ruff");
|
||||
path.push(".ruff.toml");
|
||||
if path.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
// Search for a user-specific `ruff.toml`.
|
||||
let mut path = dirs::config_dir()?;
|
||||
path.push("ruff");
|
||||
path.push("ruff.toml");
|
||||
if path.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
// Search for a user-specific `pyproject.toml`.
|
||||
let mut path = dirs::config_dir()?;
|
||||
path.push("ruff");
|
||||
path.push("pyproject.toml");
|
||||
if path.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
|
||||
pub fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||
if path.as_ref().ends_with("pyproject.toml") {
|
||||
let pyproject = parse_pyproject_toml(&path)?;
|
||||
let mut ruff = pyproject
|
||||
.tool
|
||||
.and_then(|tool| tool.ruff)
|
||||
.unwrap_or_default();
|
||||
if ruff.target_version.is_none() {
|
||||
if let Some(project) = pyproject.project {
|
||||
if let Some(requires_python) = project.requires_python {
|
||||
ruff.target_version =
|
||||
PythonVersion::get_minimum_supported_version(&requires_python);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ruff)
|
||||
} else {
|
||||
let ruff = parse_ruff_toml(path);
|
||||
if let Ok(ruff) = &ruff {
|
||||
if ruff.target_version.is_none() {
|
||||
debug!("`project.requires_python` in `pyproject.toml` will not be used to set `target_version` when using `ruff.toml`.");
|
||||
}
|
||||
}
|
||||
ruff
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::options::Options;
|
||||
use crate::pyproject::{find_settings_toml, parse_pyproject_toml, Pyproject, Tools};
|
||||
use crate::tests::test_resource_path;
|
||||
use anyhow::Result;
|
||||
use ruff::codes;
|
||||
use ruff::codes::RuleCodePrefix;
|
||||
use ruff::line_width::LineLength;
|
||||
use ruff::settings::types::PatternPrefixPair;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
#[test]
|
||||
|
||||
fn deserialize() -> Result<()> {
|
||||
let pyproject: Pyproject = toml::from_str(r#""#)?;
|
||||
assert_eq!(pyproject.tool, None);
|
||||
|
||||
let pyproject: Pyproject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(pyproject.tool, Some(Tools { ruff: None }));
|
||||
|
||||
let pyproject: Pyproject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Options::default())
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: Pyproject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Options {
|
||||
line_length: Some(LineLength::from(79)),
|
||||
..Options::default()
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: Pyproject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
exclude = ["foo.py"]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Options {
|
||||
exclude: Some(vec!["foo.py".to_string()]),
|
||||
..Options::default()
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: Pyproject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
select = ["E501"]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Options {
|
||||
select: Some(vec![codes::Pycodestyle::E501.into()]),
|
||||
..Options::default()
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: Pyproject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
extend-select = ["RUF100"]
|
||||
ignore = ["E501"]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Options {
|
||||
extend_select: Some(vec![codes::Ruff::_100.into()]),
|
||||
ignore: Some(vec![codes::Pycodestyle::E501.into()]),
|
||||
..Options::default()
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
assert!(toml::from_str::<Pyproject>(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line_length = 79
|
||||
"#,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(toml::from_str::<Pyproject>(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
select = ["E123"]
|
||||
"#,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(toml::from_str::<Pyproject>(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
other-attribute = 1
|
||||
"#,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_and_parse_pyproject_toml() -> Result<()> {
|
||||
let pyproject = find_settings_toml(test_resource_path("fixtures/__init__.py"))?.unwrap();
|
||||
assert_eq!(pyproject, test_resource_path("fixtures/pyproject.toml"));
|
||||
|
||||
let pyproject = parse_pyproject_toml(&pyproject)?;
|
||||
let config = pyproject.tool.unwrap().ruff.unwrap();
|
||||
assert_eq!(
|
||||
config,
|
||||
Options {
|
||||
line_length: Some(LineLength::from(88)),
|
||||
extend_exclude: Some(vec![
|
||||
"excluded_file.py".to_string(),
|
||||
"migrations".to_string(),
|
||||
"with_excluded_file/other_excluded_file.py".to_string(),
|
||||
]),
|
||||
per_file_ignores: Some(FxHashMap::from_iter([(
|
||||
"__init__.py".to_string(),
|
||||
vec![RuleCodePrefix::Pyflakes(codes::Pyflakes::_401).into()]
|
||||
)])),
|
||||
..Options::default()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_pattern_prefix_pair() {
|
||||
let result = PatternPrefixPair::from_str("foo:E501");
|
||||
assert!(result.is_ok());
|
||||
let result = PatternPrefixPair::from_str("foo: E501");
|
||||
assert!(result.is_ok());
|
||||
let result = PatternPrefixPair::from_str("E501:foo");
|
||||
assert!(result.is_err());
|
||||
let result = PatternPrefixPair::from_str("E501");
|
||||
assert!(result.is_err());
|
||||
let result = PatternPrefixPair::from_str("foo");
|
||||
assert!(result.is_err());
|
||||
let result = PatternPrefixPair::from_str("foo:E501:E402");
|
||||
assert!(result.is_err());
|
||||
let result = PatternPrefixPair::from_str("**/bar:E501");
|
||||
assert!(result.is_ok());
|
||||
let result = PatternPrefixPair::from_str("bar:E502");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
723
crates/ruff_workspace/src/resolver.rs
Normal file
723
crates/ruff_workspace/src/resolver.rs
Normal file
|
@ -0,0 +1,723 @@
|
|||
//! Discover Python files, and their corresponding [`Settings`], from the
|
||||
//! filesystem.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, bail};
|
||||
use ignore::{DirEntry, WalkBuilder, WalkState};
|
||||
use itertools::Itertools;
|
||||
use log::debug;
|
||||
use path_absolutize::path_dedot;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use crate::configuration::Configuration;
|
||||
use crate::pyproject;
|
||||
use crate::pyproject::settings_toml;
|
||||
use ruff::fs;
|
||||
use ruff::packaging::is_package;
|
||||
use ruff::settings::{AllSettings, Settings};
|
||||
|
||||
/// The configuration information from a `pyproject.toml` file.
|
||||
pub struct PyprojectConfig {
|
||||
/// The strategy used to discover the relevant `pyproject.toml` file for
|
||||
/// each Python file.
|
||||
pub strategy: PyprojectDiscoveryStrategy,
|
||||
/// All settings from the `pyproject.toml` file.
|
||||
pub settings: AllSettings,
|
||||
/// Absolute path to the `pyproject.toml` file. This would be `None` when
|
||||
/// either using the default settings or the `--isolated` flag is set.
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl PyprojectConfig {
|
||||
pub fn new(
|
||||
strategy: PyprojectDiscoveryStrategy,
|
||||
settings: AllSettings,
|
||||
path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
strategy,
|
||||
settings,
|
||||
path: path.map(fs::normalize_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The strategy used to discover the relevant `pyproject.toml` file for each
|
||||
/// Python file.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum PyprojectDiscoveryStrategy {
|
||||
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
|
||||
/// provided on the command-line).
|
||||
Fixed,
|
||||
/// Use the closest `pyproject.toml` file in the filesystem hierarchy, or
|
||||
/// the default settings.
|
||||
Hierarchical,
|
||||
}
|
||||
|
||||
impl PyprojectDiscoveryStrategy {
|
||||
pub const fn is_fixed(self) -> bool {
|
||||
matches!(self, PyprojectDiscoveryStrategy::Fixed)
|
||||
}
|
||||
|
||||
pub const fn is_hierarchical(self) -> bool {
|
||||
matches!(self, PyprojectDiscoveryStrategy::Hierarchical)
|
||||
}
|
||||
}
|
||||
|
||||
/// The strategy for resolving file paths in a `pyproject.toml`.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Relativity {
|
||||
/// Resolve file paths relative to the current working directory.
|
||||
Cwd,
|
||||
/// Resolve file paths relative to the directory containing the
|
||||
/// `pyproject.toml`.
|
||||
Parent,
|
||||
}
|
||||
|
||||
impl Relativity {
|
||||
pub fn resolve(self, path: &Path) -> PathBuf {
|
||||
match self {
|
||||
Relativity::Parent => path
|
||||
.parent()
|
||||
.expect("Expected pyproject.toml file to be in parent directory")
|
||||
.to_path_buf(),
|
||||
Relativity::Cwd => path_dedot::CWD.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Resolver {
|
||||
settings: BTreeMap<PathBuf, AllSettings>,
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
/// Add a resolved [`Settings`] under a given [`PathBuf`] scope.
|
||||
fn add(&mut self, path: PathBuf, settings: AllSettings) {
|
||||
self.settings.insert(path, settings);
|
||||
}
|
||||
|
||||
/// Return the appropriate [`AllSettings`] for a given [`Path`].
|
||||
pub fn resolve_all<'a>(
|
||||
&'a self,
|
||||
path: &Path,
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> &'a AllSettings {
|
||||
match pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => &pyproject_config.settings,
|
||||
PyprojectDiscoveryStrategy::Hierarchical => self
|
||||
.settings
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|(root, settings)| path.starts_with(root).then_some(settings))
|
||||
.unwrap_or(&pyproject_config.settings),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve<'a>(
|
||||
&'a self,
|
||||
path: &Path,
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> &'a Settings {
|
||||
&self.resolve_all(path, pyproject_config).lib
|
||||
}
|
||||
|
||||
/// Return a mapping from Python package to its package root.
|
||||
pub fn package_roots<'a>(
|
||||
&'a self,
|
||||
files: &[&'a Path],
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
) -> FxHashMap<&'a Path, Option<&'a Path>> {
|
||||
// Pre-populate the module cache, since the list of files could (but isn't
|
||||
// required to) contain some `__init__.py` files.
|
||||
let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default();
|
||||
for file in files {
|
||||
if file.ends_with("__init__.py") {
|
||||
if let Some(parent) = file.parent() {
|
||||
package_cache.insert(parent, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for the package root for each file.
|
||||
let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default();
|
||||
for file in files {
|
||||
let namespace_packages = &self.resolve(file, pyproject_config).namespace_packages;
|
||||
if let Some(package) = file.parent() {
|
||||
if package_roots.contains_key(package) {
|
||||
continue;
|
||||
}
|
||||
package_roots.insert(
|
||||
package,
|
||||
detect_package_root_with_cache(package, namespace_packages, &mut package_cache),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
package_roots
|
||||
}
|
||||
|
||||
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
|
||||
pub fn settings(&self) -> impl Iterator<Item = &AllSettings> {
|
||||
self.settings.values()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around `detect_package_root` to cache filesystem lookups.
|
||||
fn detect_package_root_with_cache<'a>(
|
||||
path: &'a Path,
|
||||
namespace_packages: &'a [PathBuf],
|
||||
package_cache: &mut FxHashMap<&'a Path, bool>,
|
||||
) -> Option<&'a Path> {
|
||||
let mut current = None;
|
||||
for parent in path.ancestors() {
|
||||
if !is_package_with_cache(parent, namespace_packages, package_cache) {
|
||||
return current;
|
||||
}
|
||||
current = Some(parent);
|
||||
}
|
||||
current
|
||||
}
|
||||
|
||||
/// A wrapper around `is_package` to cache filesystem lookups.
|
||||
fn is_package_with_cache<'a>(
|
||||
path: &'a Path,
|
||||
namespace_packages: &'a [PathBuf],
|
||||
package_cache: &mut FxHashMap<&'a Path, bool>,
|
||||
) -> bool {
|
||||
*package_cache
|
||||
.entry(path)
|
||||
.or_insert_with(|| is_package(path, namespace_packages))
|
||||
}
|
||||
|
||||
pub trait ConfigProcessor: Sync {
|
||||
fn process_config(&self, config: &mut Configuration);
|
||||
}
|
||||
|
||||
/// Recursively resolve a [`Configuration`] from a `pyproject.toml` file at the
|
||||
/// specified [`Path`].
|
||||
// TODO(charlie): This whole system could do with some caching. Right now, if a
|
||||
// configuration file extends another in the same path, we'll re-parse the same
|
||||
// file at least twice (possibly more than twice, since we'll also parse it when
|
||||
// resolving the "default" configuration).
|
||||
fn resolve_configuration(
|
||||
pyproject: &Path,
|
||||
relativity: Relativity,
|
||||
processor: &dyn ConfigProcessor,
|
||||
) -> Result<Configuration> {
|
||||
let mut seen = FxHashSet::default();
|
||||
let mut stack = vec![];
|
||||
let mut next = Some(fs::normalize_path(pyproject));
|
||||
while let Some(path) = next {
|
||||
if seen.contains(&path) {
|
||||
bail!("Circular dependency detected in pyproject.toml");
|
||||
}
|
||||
|
||||
// Resolve the current path.
|
||||
let options = pyproject::load_options(&path)
|
||||
.map_err(|err| anyhow!("Failed to parse `{}`: {}", path.display(), err))?;
|
||||
let project_root = relativity.resolve(&path);
|
||||
let configuration = Configuration::from_options(options, &project_root)?;
|
||||
|
||||
// If extending, continue to collect.
|
||||
next = configuration.extend.as_ref().map(|extend| {
|
||||
fs::normalize_path_to(
|
||||
extend,
|
||||
path.parent()
|
||||
.expect("Expected pyproject.toml file to be in parent directory"),
|
||||
)
|
||||
});
|
||||
|
||||
// Keep track of (1) the paths we've already resolved (to avoid cycles), and (2)
|
||||
// the base configuration for every path.
|
||||
seen.insert(path);
|
||||
stack.push(configuration);
|
||||
}
|
||||
|
||||
// Merge the configurations, in order.
|
||||
stack.reverse();
|
||||
let mut configuration = stack.pop().unwrap();
|
||||
while let Some(extend) = stack.pop() {
|
||||
configuration = configuration.combine(extend);
|
||||
}
|
||||
processor.process_config(&mut configuration);
|
||||
Ok(configuration)
|
||||
}
|
||||
|
||||
/// Extract the project root (scope) and [`Settings`] from a given
|
||||
/// `pyproject.toml`.
|
||||
fn resolve_scoped_settings(
|
||||
pyproject: &Path,
|
||||
relativity: Relativity,
|
||||
processor: &dyn ConfigProcessor,
|
||||
) -> Result<(PathBuf, AllSettings)> {
|
||||
let configuration = resolve_configuration(pyproject, relativity, processor)?;
|
||||
let project_root = relativity.resolve(pyproject);
|
||||
let settings = configuration.into_all_settings(&project_root)?;
|
||||
Ok((project_root, settings))
|
||||
}
|
||||
|
||||
/// Extract the [`Settings`] from a given `pyproject.toml` and process the
|
||||
/// configuration with the given [`ConfigProcessor`].
|
||||
pub fn resolve_settings_with_processor(
|
||||
pyproject: &Path,
|
||||
relativity: Relativity,
|
||||
processor: &dyn ConfigProcessor,
|
||||
) -> Result<AllSettings> {
|
||||
let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, processor)?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths.
|
||||
pub fn python_files_in_path(
|
||||
paths: &[PathBuf],
|
||||
pyproject_config: &PyprojectConfig,
|
||||
processor: &dyn ConfigProcessor,
|
||||
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
|
||||
// Normalize every path (e.g., convert from relative to absolute).
|
||||
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect();
|
||||
|
||||
// Search for `pyproject.toml` files in all parent directories.
|
||||
let mut resolver = Resolver::default();
|
||||
let mut seen = FxHashSet::default();
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
for path in &paths {
|
||||
for ancestor in path.ancestors() {
|
||||
if seen.insert(ancestor) {
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, Relativity::Parent, processor)?;
|
||||
resolver.add(root, settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the paths themselves are excluded.
|
||||
if pyproject_config.settings.lib.force_exclude {
|
||||
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_config));
|
||||
if paths.is_empty() {
|
||||
return Ok((vec![], resolver));
|
||||
}
|
||||
}
|
||||
|
||||
// Create the `WalkBuilder`.
|
||||
let mut builder = WalkBuilder::new(
|
||||
paths
|
||||
.get(0)
|
||||
.ok_or_else(|| anyhow!("Expected at least one path to search for Python files"))?,
|
||||
);
|
||||
for path in &paths[1..] {
|
||||
builder.add(path);
|
||||
}
|
||||
builder.standard_filters(pyproject_config.settings.lib.respect_gitignore);
|
||||
builder.hidden(false);
|
||||
let walker = builder.build_parallel();
|
||||
|
||||
// Run the `WalkParallel` to collect all Python files.
|
||||
let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(()));
|
||||
let resolver: RwLock<Resolver> = RwLock::new(resolver);
|
||||
let files: std::sync::Mutex<Vec<Result<DirEntry, ignore::Error>>> =
|
||||
std::sync::Mutex::new(vec![]);
|
||||
walker.run(|| {
|
||||
Box::new(|result| {
|
||||
// Respect our own exclusion behavior.
|
||||
if let Ok(entry) = &result {
|
||||
if entry.depth() > 0 {
|
||||
let path = entry.path();
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if !settings.exclude.is_empty()
|
||||
&& match_exclusion(path, file_name, &settings.exclude)
|
||||
{
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
return WalkState::Skip;
|
||||
} else if !settings.extend_exclude.is_empty()
|
||||
&& match_exclusion(path, file_name, &settings.extend_exclude)
|
||||
{
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
return WalkState::Skip;
|
||||
}
|
||||
} else {
|
||||
debug!("Ignored path due to error in parsing: {:?}", path);
|
||||
return WalkState::Skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for the `pyproject.toml` file in this directory, before we visit any
|
||||
// of its contents.
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
if let Ok(entry) = &result {
|
||||
if entry
|
||||
.file_type()
|
||||
.is_some_and(|file_type| file_type.is_dir())
|
||||
{
|
||||
match settings_toml(entry.path()) {
|
||||
Ok(Some(pyproject)) => match resolve_scoped_settings(
|
||||
&pyproject,
|
||||
Relativity::Parent,
|
||||
processor,
|
||||
) {
|
||||
Ok((root, settings)) => {
|
||||
resolver.write().unwrap().add(root, settings);
|
||||
}
|
||||
Err(err) => {
|
||||
*error.lock().unwrap() = Err(err);
|
||||
return WalkState::Quit;
|
||||
}
|
||||
},
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
*error.lock().unwrap() = Err(err);
|
||||
return WalkState::Quit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.as_ref().map_or(true, |entry| {
|
||||
// Ignore directories
|
||||
if entry.file_type().map_or(true, |ft| ft.is_dir()) {
|
||||
false
|
||||
} else if entry.depth() == 0 {
|
||||
// Accept all files that are passed-in directly.
|
||||
true
|
||||
} else {
|
||||
// Otherwise, check if the file is included.
|
||||
let path = entry.path();
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if settings.include.is_match(path) {
|
||||
debug!("Included path via `include`: {:?}", path);
|
||||
true
|
||||
} else if settings.extend_include.is_match(path) {
|
||||
debug!("Included path via `extend-include`: {:?}", path);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}) {
|
||||
files.lock().unwrap().push(result);
|
||||
}
|
||||
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
error.into_inner().unwrap()?;
|
||||
|
||||
Ok((files.into_inner().unwrap(), resolver.into_inner().unwrap()))
|
||||
}
|
||||
|
||||
/// Return `true` if the Python file at [`Path`] is _not_ excluded.
|
||||
pub fn python_file_at_path(
|
||||
path: &Path,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
processor: &dyn ConfigProcessor,
|
||||
) -> Result<bool> {
|
||||
if !pyproject_config.settings.lib.force_exclude {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Normalize the path (e.g., convert from relative to absolute).
|
||||
let path = fs::normalize_path(path);
|
||||
|
||||
// Search for `pyproject.toml` files in all parent directories.
|
||||
let mut resolver = Resolver::default();
|
||||
if pyproject_config.strategy.is_hierarchical() {
|
||||
for ancestor in path.ancestors() {
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, Relativity::Parent, processor)?;
|
||||
resolver.add(root, settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check exclusions.
|
||||
Ok(!is_file_excluded(&path, &resolver, pyproject_config))
|
||||
}
|
||||
|
||||
/// Return `true` if the given top-level [`Path`] should be excluded.
|
||||
fn is_file_excluded(
|
||||
path: &Path,
|
||||
resolver: &Resolver,
|
||||
pyproject_strategy: &PyprojectConfig,
|
||||
) -> bool {
|
||||
// TODO(charlie): Respect gitignore.
|
||||
for path in path.ancestors() {
|
||||
if path.file_name().is_none() {
|
||||
break;
|
||||
}
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if !settings.exclude.is_empty() && match_exclusion(path, file_name, &settings.exclude) {
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
return true;
|
||||
} else if !settings.extend_exclude.is_empty()
|
||||
&& match_exclusion(path, file_name, &settings.extend_exclude)
|
||||
{
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
debug!("Ignored path due to error in parsing: {:?}", path);
|
||||
return true;
|
||||
}
|
||||
if path == settings.project_root {
|
||||
// Bail out; we'd end up past the project root on the next iteration
|
||||
// (excludes etc. are thus "rooted" to the project).
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Return `true` if the given file should be ignored based on the exclusion
|
||||
/// criteria.
|
||||
fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
|
||||
file_path: P,
|
||||
file_basename: R,
|
||||
exclusion: &globset::GlobSet,
|
||||
) -> bool {
|
||||
exclusion.is_match(file_path) || exclusion.is_match(file_basename)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{create_dir, File};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use globset::GlobSet;
|
||||
use itertools::Itertools;
|
||||
use path_absolutize::Absolutize;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::configuration::Configuration;
|
||||
use crate::pyproject::find_settings_toml;
|
||||
use ruff::settings::types::FilePattern;
|
||||
use ruff::settings::AllSettings;
|
||||
|
||||
use crate::resolver::{
|
||||
is_file_excluded, match_exclusion, python_files_in_path, resolve_settings_with_processor,
|
||||
ConfigProcessor, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
|
||||
};
|
||||
use crate::tests::test_resource_path;
|
||||
|
||||
struct NoOpProcessor;
|
||||
|
||||
impl ConfigProcessor for NoOpProcessor {
|
||||
fn process_config(&self, _config: &mut Configuration) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rooted_exclusion() -> Result<()> {
|
||||
let package_root = test_resource_path("package");
|
||||
let resolver = Resolver::default();
|
||||
let pyproject_config = PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
resolve_settings_with_processor(
|
||||
&find_settings_toml(&package_root)?.unwrap(),
|
||||
Relativity::Parent,
|
||||
&NoOpProcessor,
|
||||
)?,
|
||||
None,
|
||||
);
|
||||
// src/app.py should not be excluded even if it lives in a hierarchy that should
|
||||
// be excluded by virtue of the pyproject.toml having `resources/*` in
|
||||
// it.
|
||||
assert!(!is_file_excluded(
|
||||
&package_root.join("src/app.py"),
|
||||
&resolver,
|
||||
&pyproject_config,
|
||||
));
|
||||
// However, resources/ignored.py should be ignored, since that `resources` is
|
||||
// beneath the package root.
|
||||
assert!(is_file_excluded(
|
||||
&package_root.join("resources/ignored.py"),
|
||||
&resolver,
|
||||
&pyproject_config,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_python_files() -> Result<()> {
|
||||
// Initialize the filesystem:
|
||||
// root
|
||||
// ├── file1.py
|
||||
// ├── dir1.py
|
||||
// │ └── file2.py
|
||||
// └── dir2.py
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let root = tmp_dir.path();
|
||||
let file1 = root.join("file1.py");
|
||||
let dir1 = root.join("dir1.py");
|
||||
let file2 = dir1.join("file2.py");
|
||||
let dir2 = root.join("dir2.py");
|
||||
File::create(&file1)?;
|
||||
create_dir(dir1)?;
|
||||
File::create(&file2)?;
|
||||
create_dir(dir2)?;
|
||||
|
||||
let (paths, _) = python_files_in_path(
|
||||
&[root.to_path_buf()],
|
||||
&PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Fixed,
|
||||
AllSettings::default(),
|
||||
None,
|
||||
),
|
||||
&NoOpProcessor,
|
||||
)?;
|
||||
let paths = paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(ignore::DirEntry::path)
|
||||
.sorted()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(paths, &[file2, file1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_exclusion(file_pattern: FilePattern) -> GlobSet {
|
||||
let mut builder = globset::GlobSetBuilder::new();
|
||||
file_pattern.add_to(&mut builder).unwrap();
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclusions() {
|
||||
let project_root = Path::new("/tmp/");
|
||||
|
||||
let path = Path::new("foo").absolutize_from(project_root).unwrap();
|
||||
let exclude = FilePattern::User(
|
||||
"foo".to_string(),
|
||||
Path::new("foo")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap()
|
||||
.to_path_buf(),
|
||||
);
|
||||
let file_path = &path;
|
||||
let file_basename = path.file_name().unwrap();
|
||||
assert!(match_exclusion(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude),
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = FilePattern::User(
|
||||
"bar".to_string(),
|
||||
Path::new("bar")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap()
|
||||
.to_path_buf(),
|
||||
);
|
||||
let file_path = &path;
|
||||
let file_basename = path.file_name().unwrap();
|
||||
assert!(match_exclusion(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude),
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = FilePattern::User(
|
||||
"baz.py".to_string(),
|
||||
Path::new("baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap()
|
||||
.to_path_buf(),
|
||||
);
|
||||
let file_path = &path;
|
||||
let file_basename = path.file_name().unwrap();
|
||||
assert!(match_exclusion(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude),
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = FilePattern::User(
|
||||
"foo/bar".to_string(),
|
||||
Path::new("foo/bar")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap()
|
||||
.to_path_buf(),
|
||||
);
|
||||
let file_path = &path;
|
||||
let file_basename = path.file_name().unwrap();
|
||||
assert!(match_exclusion(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude),
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = FilePattern::User(
|
||||
"foo/bar/baz.py".to_string(),
|
||||
Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap()
|
||||
.to_path_buf(),
|
||||
);
|
||||
let file_path = &path;
|
||||
let file_basename = path.file_name().unwrap();
|
||||
assert!(match_exclusion(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude),
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = FilePattern::User(
|
||||
"foo/bar/*.py".to_string(),
|
||||
Path::new("foo/bar/*.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap()
|
||||
.to_path_buf(),
|
||||
);
|
||||
let file_path = &path;
|
||||
let file_basename = path.file_name().unwrap();
|
||||
assert!(match_exclusion(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude),
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = FilePattern::User(
|
||||
"baz".to_string(),
|
||||
Path::new("baz")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap()
|
||||
.to_path_buf(),
|
||||
);
|
||||
let file_path = &path;
|
||||
let file_basename = path.file_name().unwrap();
|
||||
assert!(!match_exclusion(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude),
|
||||
));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue