mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +00:00

## Summary This patch inverts the defaults for [pytest-fixture-incorrect-parentheses-style (PT001)](https://docs.astral.sh/ruff/rules/pytest-fixture-incorrect-parentheses-style/) and [pytest-incorrect-mark-parentheses-style (PT003)](https://docs.astral.sh/ruff/rules/pytest-incorrect-mark-parentheses-style/) to prefer dropping superfluous parentheses. Presently, Ruff defaults to adding superfluous parentheses on pytest mark and fixture decorators for documented purpose of consistency; for example, ```diff import pytest -@pytest.mark.foo +@pytest.mark.foo() def test_bar(): ... ``` This behaviour is counter to the official pytest recommendation and diverges from the flake8-pytest-style plugin as of version 2.0.0 (see https://github.com/m-burst/flake8-pytest-style/issues/272). Seeing as either default satisfies the documented benefit of consistency across a codebase, it makes sense to change the behaviour to be consistent with pytest and the flake8 plugin as well. This change is breaking, so is gated behind preview (at least under my understanding of Ruff versioning). The implementation of this gating feature is a bit hacky, but seemed to be the least disruptive solution without performing invasive surgery on the `#[option()]` macro. Related to #8796. ### Caveat Whilst updating the documentation, I sought to reference the pytest recommendation to drop superfluous parentheses, but couldn't find any official instruction beyond it being a revealed preference within the pytest documentation code examples (as well as the linked issues from a core pytest developer). Thus, the wording of the preference is deliberately timid; it's to cohere with pytest rather than follow an explicit guidance. ## Test Plan `cargo nextest run` I also ran ```sh cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT001.py --no-cache --diff --select PT001 ``` and compared against it with `--preview` to verify that the default does change under preview (I also repeated this with `echo '[tool.ruff]\npreview = true' > pyproject.toml` to verify that it works with a configuration file). --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
1983 lines
73 KiB
Rust
1983 lines
73 KiB
Rust
//! 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::num::{NonZeroU16, NonZeroU8};
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use glob::{glob, GlobError, Paths, PatternError};
|
|
use itertools::Itertools;
|
|
use regex::Regex;
|
|
use rustc_hash::{FxHashMap, FxHashSet};
|
|
use shellexpand;
|
|
use shellexpand::LookupError;
|
|
use strum::IntoEnumIterator;
|
|
|
|
use ruff_cache::cache_dir;
|
|
use ruff_formatter::IndentStyle;
|
|
use ruff_linter::line_width::{IndentWidth, LineLength};
|
|
use ruff_linter::registry::RuleNamespace;
|
|
use ruff_linter::registry::{Rule, RuleSet, INCOMPATIBLE_CODES};
|
|
use ruff_linter::rule_selector::{PreviewOptions, Specificity};
|
|
use ruff_linter::rules::{flake8_pytest_style, pycodestyle};
|
|
use ruff_linter::settings::fix_safety_table::FixSafetyTable;
|
|
use ruff_linter::settings::rule_table::RuleTable;
|
|
use ruff_linter::settings::types::{
|
|
CompiledPerFileIgnoreList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat,
|
|
PerFileIgnore, PreviewMode, PythonVersion, RequiredVersion, UnsafeFixes,
|
|
};
|
|
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
|
|
use ruff_linter::{
|
|
fs, warn_user_once, warn_user_once_by_id, warn_user_once_by_message, RuleSelector,
|
|
RUFF_PKG_VERSION,
|
|
};
|
|
use ruff_python_formatter::{
|
|
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, QuoteStyle,
|
|
};
|
|
|
|
use crate::options::{
|
|
Flake8AnnotationsOptions, Flake8BanditOptions, Flake8BooleanTrapOptions, Flake8BugbearOptions,
|
|
Flake8BuiltinsOptions, Flake8ComprehensionsOptions, Flake8CopyrightOptions,
|
|
Flake8ErrMsgOptions, Flake8GetTextOptions, Flake8ImplicitStrConcatOptions,
|
|
Flake8ImportConventionsOptions, Flake8PytestStyleOptions, Flake8QuotesOptions,
|
|
Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions,
|
|
Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions,
|
|
McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
|
|
PydocstyleOptions, PyflakesOptions, PylintOptions,
|
|
};
|
|
use crate::settings::{
|
|
FileResolverSettings, FormatterSettings, LineEnding, Settings, EXCLUDE, INCLUDE,
|
|
};
|
|
|
|
#[derive(Clone, 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, Eq, PartialEq, is_macro::Is)]
|
|
pub enum RuleSelectorKind {
|
|
/// Enables the selected rules
|
|
Enable,
|
|
/// Disables the selected rules
|
|
Disable,
|
|
/// Modifies the behavior of selected rules
|
|
Modify,
|
|
}
|
|
|
|
impl RuleSelection {
|
|
pub fn selectors_by_kind(&self) -> impl Iterator<Item = (RuleSelectorKind, &RuleSelector)> {
|
|
self.select
|
|
.iter()
|
|
.flatten()
|
|
.map(|selector| (RuleSelectorKind::Enable, selector))
|
|
.chain(
|
|
self.fixable
|
|
.iter()
|
|
.flatten()
|
|
.map(|selector| (RuleSelectorKind::Modify, selector)),
|
|
)
|
|
.chain(
|
|
self.ignore
|
|
.iter()
|
|
.map(|selector| (RuleSelectorKind::Disable, selector)),
|
|
)
|
|
.chain(
|
|
self.extend_select
|
|
.iter()
|
|
.map(|selector| (RuleSelectorKind::Enable, selector)),
|
|
)
|
|
.chain(
|
|
self.unfixable
|
|
.iter()
|
|
.map(|selector| (RuleSelectorKind::Modify, selector)),
|
|
)
|
|
.chain(
|
|
self.extend_fixable
|
|
.iter()
|
|
.map(|selector| (RuleSelectorKind::Modify, selector)),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct Configuration {
|
|
// Global options
|
|
pub cache_dir: Option<PathBuf>,
|
|
pub extend: Option<PathBuf>,
|
|
pub fix: Option<bool>,
|
|
pub fix_only: Option<bool>,
|
|
pub unsafe_fixes: Option<UnsafeFixes>,
|
|
pub output_format: Option<OutputFormat>,
|
|
pub preview: Option<PreviewMode>,
|
|
pub required_version: Option<RequiredVersion>,
|
|
pub extension: Option<ExtensionMapping>,
|
|
pub show_fixes: Option<bool>,
|
|
|
|
// File resolver options
|
|
pub exclude: Option<Vec<FilePattern>>,
|
|
pub extend_exclude: Vec<FilePattern>,
|
|
pub extend_include: Vec<FilePattern>,
|
|
pub force_exclude: Option<bool>,
|
|
pub include: Option<Vec<FilePattern>>,
|
|
pub respect_gitignore: Option<bool>,
|
|
|
|
// Generic python options settings
|
|
pub builtins: Option<Vec<String>>,
|
|
pub namespace_packages: Option<Vec<PathBuf>>,
|
|
pub src: Option<Vec<PathBuf>>,
|
|
pub target_version: Option<PythonVersion>,
|
|
|
|
// Global formatting options
|
|
pub line_length: Option<LineLength>,
|
|
pub indent_width: Option<IndentWidth>,
|
|
|
|
pub lint: LintConfiguration,
|
|
pub format: FormatConfiguration,
|
|
}
|
|
|
|
impl Configuration {
|
|
pub fn into_settings(self, project_root: &Path) -> Result<Settings> {
|
|
if let Some(required_version) = &self.required_version {
|
|
let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION)
|
|
.expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
|
|
if !required_version.contains(&ruff_pkg_version) {
|
|
return Err(anyhow!(
|
|
"Required version `{}` does not match the running version `{}`",
|
|
required_version,
|
|
RUFF_PKG_VERSION
|
|
));
|
|
}
|
|
}
|
|
|
|
let target_version = self.target_version.unwrap_or_default();
|
|
let global_preview = self.preview.unwrap_or_default();
|
|
|
|
let format = self.format;
|
|
let format_defaults = FormatterSettings::default();
|
|
|
|
let quote_style = format.quote_style.unwrap_or(format_defaults.quote_style);
|
|
let format_preview = match format.preview.unwrap_or(global_preview) {
|
|
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
|
|
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
|
|
};
|
|
|
|
let formatter = FormatterSettings {
|
|
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
|
|
extension: self.extension.clone().unwrap_or_default(),
|
|
preview: format_preview,
|
|
target_version: match target_version {
|
|
PythonVersion::Py37 => ruff_python_formatter::PythonVersion::Py37,
|
|
PythonVersion::Py38 => ruff_python_formatter::PythonVersion::Py38,
|
|
PythonVersion::Py39 => ruff_python_formatter::PythonVersion::Py39,
|
|
PythonVersion::Py310 => ruff_python_formatter::PythonVersion::Py310,
|
|
PythonVersion::Py311 => ruff_python_formatter::PythonVersion::Py311,
|
|
PythonVersion::Py312 => ruff_python_formatter::PythonVersion::Py312,
|
|
PythonVersion::Py313 => ruff_python_formatter::PythonVersion::Py313,
|
|
},
|
|
line_width: self
|
|
.line_length
|
|
.map_or(format_defaults.line_width, |length| {
|
|
ruff_formatter::LineWidth::from(NonZeroU16::from(length))
|
|
}),
|
|
line_ending: format.line_ending.unwrap_or(format_defaults.line_ending),
|
|
indent_style: format.indent_style.unwrap_or(format_defaults.indent_style),
|
|
indent_width: self
|
|
.indent_width
|
|
.map_or(format_defaults.indent_width, |tab_size| {
|
|
ruff_formatter::IndentWidth::from(NonZeroU8::from(tab_size))
|
|
}),
|
|
quote_style,
|
|
magic_trailing_comma: format
|
|
.magic_trailing_comma
|
|
.unwrap_or(format_defaults.magic_trailing_comma),
|
|
docstring_code_format: format
|
|
.docstring_code_format
|
|
.unwrap_or(format_defaults.docstring_code_format),
|
|
docstring_code_line_width: format
|
|
.docstring_code_line_width
|
|
.unwrap_or(format_defaults.docstring_code_line_width),
|
|
};
|
|
|
|
let lint = self.lint;
|
|
let lint_preview = lint.preview.unwrap_or(global_preview);
|
|
|
|
let line_length = self.line_length.unwrap_or_default();
|
|
|
|
Ok(Settings {
|
|
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),
|
|
unsafe_fixes: self.unsafe_fixes.unwrap_or_default(),
|
|
output_format: self.output_format.unwrap_or_default(),
|
|
show_fixes: self.show_fixes.unwrap_or(false),
|
|
|
|
file_resolver: FileResolverSettings {
|
|
exclude: FilePatternSet::try_from_iter(
|
|
self.exclude.unwrap_or_else(|| EXCLUDE.to_vec()),
|
|
)?,
|
|
extend_exclude: FilePatternSet::try_from_iter(self.extend_exclude)?,
|
|
extend_include: FilePatternSet::try_from_iter(self.extend_include)?,
|
|
force_exclude: self.force_exclude.unwrap_or(false),
|
|
include: FilePatternSet::try_from_iter(
|
|
self.include.unwrap_or_else(|| INCLUDE.to_vec()),
|
|
)?,
|
|
respect_gitignore: self.respect_gitignore.unwrap_or(true),
|
|
project_root: project_root.to_path_buf(),
|
|
},
|
|
|
|
#[allow(deprecated)]
|
|
linter: LinterSettings {
|
|
rules: lint.as_rule_table(lint_preview)?,
|
|
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
|
|
extension: self.extension.unwrap_or_default(),
|
|
preview: lint_preview,
|
|
target_version,
|
|
project_root: project_root.to_path_buf(),
|
|
allowed_confusables: lint
|
|
.allowed_confusables
|
|
.map(FxHashSet::from_iter)
|
|
.unwrap_or_default(),
|
|
builtins: self.builtins.unwrap_or_default(),
|
|
dummy_variable_rgx: lint
|
|
.dummy_variable_rgx
|
|
.unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()),
|
|
external: lint.external.unwrap_or_default(),
|
|
ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or(true),
|
|
line_length,
|
|
tab_size: self.indent_width.unwrap_or_default(),
|
|
namespace_packages: self.namespace_packages.unwrap_or_default(),
|
|
per_file_ignores: CompiledPerFileIgnoreList::resolve(
|
|
lint.per_file_ignores
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.chain(lint.extend_per_file_ignores)
|
|
.collect(),
|
|
)?,
|
|
|
|
fix_safety: FixSafetyTable::from_rule_selectors(
|
|
&lint.extend_safe_fixes,
|
|
&lint.extend_unsafe_fixes,
|
|
&PreviewOptions {
|
|
mode: lint_preview,
|
|
require_explicit: false,
|
|
},
|
|
),
|
|
|
|
src: self.src.unwrap_or_else(|| vec![project_root.to_path_buf()]),
|
|
explicit_preview_rules: lint.explicit_preview_rules.unwrap_or_default(),
|
|
|
|
task_tags: lint
|
|
.task_tags
|
|
.unwrap_or_else(|| TASK_TAGS.iter().map(ToString::to_string).collect()),
|
|
logger_objects: lint.logger_objects.unwrap_or_default(),
|
|
typing_modules: lint.typing_modules.unwrap_or_default(),
|
|
// Plugins
|
|
flake8_annotations: lint
|
|
.flake8_annotations
|
|
.map(Flake8AnnotationsOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_bandit: lint
|
|
.flake8_bandit
|
|
.map(Flake8BanditOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_boolean_trap: lint
|
|
.flake8_boolean_trap
|
|
.map(Flake8BooleanTrapOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_bugbear: lint
|
|
.flake8_bugbear
|
|
.map(Flake8BugbearOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_builtins: lint
|
|
.flake8_builtins
|
|
.map(Flake8BuiltinsOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_comprehensions: lint
|
|
.flake8_comprehensions
|
|
.map(Flake8ComprehensionsOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_copyright: lint
|
|
.flake8_copyright
|
|
.map(Flake8CopyrightOptions::try_into_settings)
|
|
.transpose()?
|
|
.unwrap_or_default(),
|
|
flake8_errmsg: lint
|
|
.flake8_errmsg
|
|
.map(Flake8ErrMsgOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_implicit_str_concat: lint
|
|
.flake8_implicit_str_concat
|
|
.map(Flake8ImplicitStrConcatOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_import_conventions: lint
|
|
.flake8_import_conventions
|
|
.map(Flake8ImportConventionsOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_pytest_style: lint
|
|
.flake8_pytest_style
|
|
.map(|options| {
|
|
Flake8PytestStyleOptions::try_into_settings(options, lint_preview)
|
|
})
|
|
.transpose()?
|
|
.unwrap_or_else(|| {
|
|
flake8_pytest_style::settings::Settings::resolve_default(lint_preview)
|
|
}),
|
|
flake8_quotes: lint
|
|
.flake8_quotes
|
|
.map(Flake8QuotesOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_self: lint
|
|
.flake8_self
|
|
.map(Flake8SelfOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_tidy_imports: lint
|
|
.flake8_tidy_imports
|
|
.map(Flake8TidyImportsOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_type_checking: lint
|
|
.flake8_type_checking
|
|
.map(Flake8TypeCheckingOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_unused_arguments: lint
|
|
.flake8_unused_arguments
|
|
.map(Flake8UnusedArgumentsOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
flake8_gettext: lint
|
|
.flake8_gettext
|
|
.map(Flake8GetTextOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
isort: lint
|
|
.isort
|
|
.map(IsortOptions::try_into_settings)
|
|
.transpose()?
|
|
.unwrap_or_default(),
|
|
mccabe: lint
|
|
.mccabe
|
|
.map(McCabeOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
pep8_naming: lint
|
|
.pep8_naming
|
|
.map(Pep8NamingOptions::try_into_settings)
|
|
.transpose()?
|
|
.unwrap_or_default(),
|
|
pycodestyle: if let Some(pycodestyle) = lint.pycodestyle {
|
|
pycodestyle.into_settings(line_length)
|
|
} else {
|
|
pycodestyle::settings::Settings {
|
|
max_line_length: line_length,
|
|
..pycodestyle::settings::Settings::default()
|
|
}
|
|
},
|
|
pydocstyle: lint
|
|
.pydocstyle
|
|
.map(PydocstyleOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
pyflakes: lint
|
|
.pyflakes
|
|
.map(PyflakesOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
pylint: lint
|
|
.pylint
|
|
.map(PylintOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
pyupgrade: lint
|
|
.pyupgrade
|
|
.map(PyUpgradeOptions::into_settings)
|
|
.unwrap_or_default(),
|
|
},
|
|
|
|
formatter,
|
|
})
|
|
}
|
|
|
|
/// Convert the [`Options`] read from the given [`Path`] into a [`Configuration`].
|
|
/// If `None` is supplied for `path`, it indicates that the `Options` instance
|
|
/// was created via "inline TOML" from the `--config` flag
|
|
pub fn from_options(
|
|
options: Options,
|
|
path: Option<&Path>,
|
|
project_root: &Path,
|
|
) -> Result<Self> {
|
|
warn_about_deprecated_top_level_lint_options(&options.lint_top_level.0, path);
|
|
|
|
let lint = if let Some(mut lint) = options.lint {
|
|
lint.common = lint.common.combine(options.lint_top_level.0);
|
|
lint
|
|
} else {
|
|
LintOptions {
|
|
common: options.lint_top_level.0,
|
|
..LintOptions::default()
|
|
}
|
|
};
|
|
|
|
#[allow(deprecated)]
|
|
if options.tab_size.is_some() {
|
|
let config_to_update = path.map_or_else(
|
|
|| String::from("your `--config` CLI arguments"),
|
|
|path| format!("`{}`", fs::relativize_path(path)),
|
|
);
|
|
return Err(anyhow!("The `tab-size` option has been renamed to `indent-width` to emphasize that it configures the indentation used by the formatter as well as the tab width. Please update {config_to_update} to use `indent-width = <value>` instead."));
|
|
}
|
|
|
|
#[allow(deprecated)]
|
|
if options.output_format == Some(OutputFormat::Text) {
|
|
let config_to_update = path.map_or_else(
|
|
|| String::from("your `--config` CLI arguments"),
|
|
|path| format!("`{}`", fs::relativize_path(path)),
|
|
);
|
|
return Err(anyhow!(
|
|
r#"The option `output_format=text` is no longer supported. Update {config_to_update} to use `output-format="concise"` or `output-format="full"` instead."#
|
|
));
|
|
}
|
|
|
|
Ok(Self {
|
|
builtins: options.builtins,
|
|
cache_dir: options
|
|
.cache_dir
|
|
.map(|dir| {
|
|
let dir = shellexpand::full(&dir);
|
|
dir.map(|dir| fs::normalize_path_to(dir.as_ref(), project_root))
|
|
})
|
|
.transpose()
|
|
.map_err(|e| anyhow!("Invalid `cache-dir` 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(),
|
|
include: options.include.map(|paths| {
|
|
paths
|
|
.into_iter()
|
|
.map(|pattern| {
|
|
let absolute = fs::normalize_path_to(&pattern, project_root);
|
|
FilePattern::User(pattern, absolute)
|
|
})
|
|
.collect()
|
|
}),
|
|
fix: options.fix,
|
|
fix_only: options.fix_only,
|
|
unsafe_fixes: options.unsafe_fixes.map(UnsafeFixes::from),
|
|
output_format: options.output_format,
|
|
force_exclude: options.force_exclude,
|
|
line_length: options.line_length,
|
|
indent_width: options.indent_width,
|
|
namespace_packages: options
|
|
.namespace_packages
|
|
.map(|namespace_package| resolve_src(&namespace_package, project_root))
|
|
.transpose()?,
|
|
preview: options.preview.map(PreviewMode::from),
|
|
required_version: options.required_version,
|
|
respect_gitignore: options.respect_gitignore,
|
|
show_fixes: options.show_fixes,
|
|
src: options
|
|
.src
|
|
.map(|src| resolve_src(&src, project_root))
|
|
.transpose()?,
|
|
target_version: options.target_version,
|
|
// `--extension` is a hidden command-line argument that isn't supported in configuration
|
|
// files at present.
|
|
extension: None,
|
|
|
|
lint: LintConfiguration::from_options(lint, project_root)?,
|
|
format: FormatConfiguration::from_options(
|
|
options.format.unwrap_or_default(),
|
|
project_root,
|
|
)?,
|
|
})
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn combine(self, config: Self) -> Self {
|
|
Self {
|
|
builtins: self.builtins.or(config.builtins),
|
|
cache_dir: self.cache_dir.or(config.cache_dir),
|
|
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(),
|
|
include: self.include.or(config.include),
|
|
fix: self.fix.or(config.fix),
|
|
fix_only: self.fix_only.or(config.fix_only),
|
|
unsafe_fixes: self.unsafe_fixes.or(config.unsafe_fixes),
|
|
output_format: self.output_format.or(config.output_format),
|
|
force_exclude: self.force_exclude.or(config.force_exclude),
|
|
line_length: self.line_length.or(config.line_length),
|
|
indent_width: self.indent_width.or(config.indent_width),
|
|
namespace_packages: self.namespace_packages.or(config.namespace_packages),
|
|
required_version: self.required_version.or(config.required_version),
|
|
respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
|
|
show_fixes: self.show_fixes.or(config.show_fixes),
|
|
src: self.src.or(config.src),
|
|
target_version: self.target_version.or(config.target_version),
|
|
preview: self.preview.or(config.preview),
|
|
extension: self.extension.or(config.extension),
|
|
|
|
lint: self.lint.combine(config.lint),
|
|
format: self.format.combine(config.format),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct LintConfiguration {
|
|
pub exclude: Option<Vec<FilePattern>>,
|
|
pub preview: Option<PreviewMode>,
|
|
|
|
// Rule selection
|
|
pub extend_per_file_ignores: Vec<PerFileIgnore>,
|
|
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
|
|
pub rule_selections: Vec<RuleSelection>,
|
|
pub explicit_preview_rules: Option<bool>,
|
|
|
|
// Fix configuration
|
|
pub extend_unsafe_fixes: Vec<RuleSelector>,
|
|
pub extend_safe_fixes: Vec<RuleSelector>,
|
|
|
|
// Global lint settings
|
|
pub allowed_confusables: Option<Vec<char>>,
|
|
pub dummy_variable_rgx: Option<Regex>,
|
|
pub external: Option<Vec<String>>,
|
|
pub ignore_init_module_imports: Option<bool>,
|
|
pub logger_objects: Option<Vec<String>>,
|
|
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_boolean_trap: Option<Flake8BooleanTrapOptions>,
|
|
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 LintConfiguration {
|
|
fn from_options(options: LintOptions, project_root: &Path) -> Result<Self> {
|
|
#[allow(deprecated)]
|
|
let ignore = options
|
|
.common
|
|
.ignore
|
|
.into_iter()
|
|
.flatten()
|
|
.chain(options.common.extend_ignore.into_iter().flatten())
|
|
.collect();
|
|
#[allow(deprecated)]
|
|
let unfixable = options
|
|
.common
|
|
.unfixable
|
|
.into_iter()
|
|
.flatten()
|
|
.chain(options.common.extend_unfixable.into_iter().flatten())
|
|
.collect();
|
|
|
|
#[allow(deprecated)]
|
|
let ignore_init_module_imports = {
|
|
if options.common.ignore_init_module_imports.is_some() {
|
|
warn_user_once!("The `ignore-init-module-imports` option is deprecated and will be removed in a future release. Ruff's handling of imports in `__init__.py` files has been improved (in preview) and unused imports will always be flagged.");
|
|
}
|
|
options.common.ignore_init_module_imports
|
|
};
|
|
|
|
Ok(LintConfiguration {
|
|
exclude: options.exclude.map(|paths| {
|
|
paths
|
|
.into_iter()
|
|
.map(|pattern| {
|
|
let absolute = fs::normalize_path_to(&pattern, project_root);
|
|
FilePattern::User(pattern, absolute)
|
|
})
|
|
.collect()
|
|
}),
|
|
preview: options.preview.map(PreviewMode::from),
|
|
|
|
rule_selections: vec![RuleSelection {
|
|
select: options.common.select,
|
|
ignore,
|
|
extend_select: options.common.extend_select.unwrap_or_default(),
|
|
fixable: options.common.fixable,
|
|
unfixable,
|
|
extend_fixable: options.common.extend_fixable.unwrap_or_default(),
|
|
}],
|
|
extend_safe_fixes: options.common.extend_safe_fixes.unwrap_or_default(),
|
|
extend_unsafe_fixes: options.common.extend_unsafe_fixes.unwrap_or_default(),
|
|
allowed_confusables: options.common.allowed_confusables,
|
|
dummy_variable_rgx: options
|
|
.common
|
|
.dummy_variable_rgx
|
|
.map(|pattern| Regex::new(&pattern))
|
|
.transpose()
|
|
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
|
|
extend_per_file_ignores: options
|
|
.common
|
|
.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.common.external,
|
|
ignore_init_module_imports,
|
|
explicit_preview_rules: options.common.explicit_preview_rules,
|
|
per_file_ignores: options.common.per_file_ignores.map(|per_file_ignores| {
|
|
per_file_ignores
|
|
.into_iter()
|
|
.map(|(pattern, prefixes)| {
|
|
PerFileIgnore::new(pattern, &prefixes, Some(project_root))
|
|
})
|
|
.collect()
|
|
}),
|
|
task_tags: options.common.task_tags,
|
|
logger_objects: options.common.logger_objects,
|
|
typing_modules: options.common.typing_modules,
|
|
// Plugins
|
|
flake8_annotations: options.common.flake8_annotations,
|
|
flake8_bandit: options.common.flake8_bandit,
|
|
flake8_boolean_trap: options.common.flake8_boolean_trap,
|
|
flake8_bugbear: options.common.flake8_bugbear,
|
|
flake8_builtins: options.common.flake8_builtins,
|
|
flake8_comprehensions: options.common.flake8_comprehensions,
|
|
flake8_copyright: options.common.flake8_copyright,
|
|
flake8_errmsg: options.common.flake8_errmsg,
|
|
flake8_gettext: options.common.flake8_gettext,
|
|
flake8_implicit_str_concat: options.common.flake8_implicit_str_concat,
|
|
flake8_import_conventions: options.common.flake8_import_conventions,
|
|
flake8_pytest_style: options.common.flake8_pytest_style,
|
|
flake8_quotes: options.common.flake8_quotes,
|
|
flake8_self: options.common.flake8_self,
|
|
flake8_tidy_imports: options.common.flake8_tidy_imports,
|
|
flake8_type_checking: options.common.flake8_type_checking,
|
|
flake8_unused_arguments: options.common.flake8_unused_arguments,
|
|
isort: options.common.isort,
|
|
mccabe: options.common.mccabe,
|
|
pep8_naming: options.common.pep8_naming,
|
|
pycodestyle: options.common.pycodestyle,
|
|
pydocstyle: options.common.pydocstyle,
|
|
pyflakes: options.common.pyflakes,
|
|
pylint: options.common.pylint,
|
|
pyupgrade: options.common.pyupgrade,
|
|
})
|
|
}
|
|
|
|
fn as_rule_table(&self, preview: PreviewMode) -> Result<RuleTable> {
|
|
let preview = PreviewOptions {
|
|
mode: preview,
|
|
require_explicit: self.explicit_preview_rules.unwrap_or_default(),
|
|
};
|
|
|
|
// The select_set keeps track of which rules have been selected.
|
|
let mut select_set: RuleSet = DEFAULT_SELECTORS
|
|
.iter()
|
|
.flat_map(|selector| selector.rules(&preview))
|
|
.collect();
|
|
|
|
// The fixable set keeps track of which rules are fixable.
|
|
let mut fixable_set: RuleSet = RuleSelector::All.all_rules().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;
|
|
|
|
// Store selectors for displaying warnings
|
|
let mut redirects = FxHashMap::default();
|
|
let mut deprecated_selectors = FxHashSet::default();
|
|
let mut removed_selectors = FxHashSet::default();
|
|
let mut ignored_preview_selectors = FxHashSet::default();
|
|
|
|
// Track which docstring rules are specifically enabled
|
|
// which lets us override the docstring convention ignore-list
|
|
let mut docstring_overrides: FxHashSet<Rule> = FxHashSet::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 mut docstring_override_updates: FxHashSet<Rule> = FxHashSet::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.rules(&preview) {
|
|
select_map_updates.insert(rule, true);
|
|
|
|
if spec == Specificity::Rule {
|
|
docstring_override_updates.insert(rule);
|
|
}
|
|
}
|
|
}
|
|
for selector in selection
|
|
.ignore
|
|
.iter()
|
|
.chain(carriedover_ignores.into_iter().flatten())
|
|
.filter(|s| s.specificity() == spec)
|
|
{
|
|
for rule in selector.rules(&preview) {
|
|
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.all_rules() {
|
|
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.all_rules() {
|
|
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);
|
|
}
|
|
|
|
docstring_overrides = docstring_override_updates;
|
|
} else {
|
|
// Otherwise we apply the updates on top of the existing select_set.
|
|
for (rule, enabled) in select_map_updates {
|
|
select_set.set(rule, enabled);
|
|
}
|
|
|
|
for rule in docstring_override_updates {
|
|
docstring_overrides.insert(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 {
|
|
fixable_set.set(rule, enabled);
|
|
}
|
|
}
|
|
|
|
// Check for selections that require a warning
|
|
for (kind, selector) in selection.selectors_by_kind() {
|
|
// Some of these checks are only for `Kind::Enable` which means only `--select` will warn
|
|
// and use with, e.g., `--ignore` or `--fixable` is okay
|
|
|
|
// Unstable rules
|
|
if preview.mode.is_disabled() && kind.is_enable() {
|
|
// Check if the selector is empty because preview mode is disabled
|
|
if selector.rules(&preview).next().is_none()
|
|
&& selector
|
|
.rules(&PreviewOptions {
|
|
mode: PreviewMode::Enabled,
|
|
require_explicit: preview.require_explicit,
|
|
})
|
|
.next()
|
|
.is_some()
|
|
{
|
|
ignored_preview_selectors.insert(selector);
|
|
}
|
|
}
|
|
|
|
// Deprecated rules
|
|
if kind.is_enable() && selector.is_exact() {
|
|
if selector.all_rules().all(|rule| rule.is_deprecated()) {
|
|
deprecated_selectors.insert(selector.clone());
|
|
}
|
|
}
|
|
|
|
// Removed rules
|
|
if selector.is_exact() {
|
|
if selector.all_rules().all(|rule| rule.is_removed()) {
|
|
removed_selectors.insert(selector);
|
|
}
|
|
}
|
|
|
|
// Redirected rules
|
|
if let RuleSelector::Prefix {
|
|
prefix,
|
|
redirected_from: Some(redirect_from),
|
|
}
|
|
| RuleSelector::Rule {
|
|
prefix,
|
|
redirected_from: Some(redirect_from),
|
|
} = selector
|
|
{
|
|
redirects.insert(redirect_from, prefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
let removed_selectors = removed_selectors.iter().sorted().collect::<Vec<_>>();
|
|
match removed_selectors.as_slice() {
|
|
[] => (),
|
|
[selection] => {
|
|
let (prefix, code) = selection.prefix_and_code();
|
|
return Err(anyhow!(
|
|
"Rule `{prefix}{code}` was removed and cannot be selected."
|
|
));
|
|
}
|
|
[..] => {
|
|
let mut message =
|
|
"The following rules have been removed and cannot be selected:".to_string();
|
|
for selection in removed_selectors {
|
|
let (prefix, code) = selection.prefix_and_code();
|
|
message.push_str("\n - ");
|
|
message.push_str(prefix);
|
|
message.push_str(code);
|
|
}
|
|
message.push('\n');
|
|
return Err(anyhow!(message));
|
|
}
|
|
}
|
|
|
|
for (from, target) in redirects.iter().sorted_by_key(|item| item.0) {
|
|
// TODO(martin): This belongs into the ruff crate.
|
|
warn_user_once_by_id!(
|
|
from,
|
|
"`{from}` has been remapped to `{}{}`.",
|
|
target.linter().common_prefix(),
|
|
target.short_code()
|
|
);
|
|
}
|
|
|
|
if preview.mode.is_disabled() {
|
|
for selection in deprecated_selectors.iter().sorted() {
|
|
let (prefix, code) = selection.prefix_and_code();
|
|
let rule = format!("{prefix}{code}");
|
|
let mut message =
|
|
format!("Rule `{rule}` is deprecated and will be removed in a future release.");
|
|
if matches!(rule.as_str(), "E999") {
|
|
message.push_str(" Syntax errors will always be shown regardless of whether this rule is selected or not.");
|
|
}
|
|
warn_user_once_by_message!("{message}");
|
|
}
|
|
} else {
|
|
let deprecated_selectors = deprecated_selectors.iter().sorted().collect::<Vec<_>>();
|
|
match deprecated_selectors.as_slice() {
|
|
[] => (),
|
|
[selection] => {
|
|
let (prefix, code) = selection.prefix_and_code();
|
|
return Err(anyhow!("Selection of deprecated rule `{prefix}{code}` is not allowed when preview is enabled."));
|
|
}
|
|
[..] => {
|
|
let mut message = "Selection of deprecated rules is not allowed when preview is enabled. Remove selection of:".to_string();
|
|
for selection in deprecated_selectors {
|
|
let (prefix, code) = selection.prefix_and_code();
|
|
message.push_str("\n\t- ");
|
|
message.push_str(prefix);
|
|
message.push_str(code);
|
|
}
|
|
message.push('\n');
|
|
return Err(anyhow!(message));
|
|
}
|
|
}
|
|
}
|
|
|
|
for selection in ignored_preview_selectors.iter().sorted() {
|
|
let (prefix, code) = selection.prefix_and_code();
|
|
warn_user_once_by_message!(
|
|
"Selection `{prefix}{code}` has no effect because preview is not enabled.",
|
|
);
|
|
}
|
|
|
|
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, disable any incompatible error
|
|
// codes unless we are specifically overridden.
|
|
if let Some(convention) = self
|
|
.pydocstyle
|
|
.as_ref()
|
|
.and_then(|pydocstyle| pydocstyle.convention)
|
|
{
|
|
for rule in convention.rules_to_be_ignored() {
|
|
if !docstring_overrides.contains(rule) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
Ok(rules)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn combine(self, config: Self) -> Self {
|
|
let mut rule_selections = config.rule_selections;
|
|
rule_selections.extend(self.rule_selections);
|
|
|
|
let mut extend_safe_fixes = config.extend_safe_fixes;
|
|
extend_safe_fixes.extend(self.extend_safe_fixes);
|
|
|
|
let mut extend_unsafe_fixes = config.extend_unsafe_fixes;
|
|
extend_unsafe_fixes.extend(self.extend_unsafe_fixes);
|
|
|
|
let mut extend_per_file_ignores = config.extend_per_file_ignores;
|
|
extend_per_file_ignores.extend(self.extend_per_file_ignores);
|
|
|
|
Self {
|
|
exclude: self.exclude.or(config.exclude),
|
|
preview: self.preview.or(config.preview),
|
|
rule_selections,
|
|
extend_safe_fixes,
|
|
extend_unsafe_fixes,
|
|
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
|
|
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
|
|
extend_per_file_ignores,
|
|
external: self.external.or(config.external),
|
|
ignore_init_module_imports: self
|
|
.ignore_init_module_imports
|
|
.or(config.ignore_init_module_imports),
|
|
logger_objects: self.logger_objects.or(config.logger_objects),
|
|
per_file_ignores: self.per_file_ignores.or(config.per_file_ignores),
|
|
explicit_preview_rules: self
|
|
.explicit_preview_rules
|
|
.or(config.explicit_preview_rules),
|
|
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_boolean_trap: self.flake8_boolean_trap.combine(config.flake8_boolean_trap),
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct FormatConfiguration {
|
|
pub exclude: Option<Vec<FilePattern>>,
|
|
pub preview: Option<PreviewMode>,
|
|
pub extension: Option<ExtensionMapping>,
|
|
|
|
pub indent_style: Option<IndentStyle>,
|
|
pub quote_style: Option<QuoteStyle>,
|
|
pub magic_trailing_comma: Option<MagicTrailingComma>,
|
|
pub line_ending: Option<LineEnding>,
|
|
pub docstring_code_format: Option<DocstringCode>,
|
|
pub docstring_code_line_width: Option<DocstringCodeLineWidth>,
|
|
}
|
|
|
|
impl FormatConfiguration {
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn from_options(options: FormatOptions, project_root: &Path) -> Result<Self> {
|
|
Ok(Self {
|
|
// `--extension` is a hidden command-line argument that isn't supported in configuration
|
|
// files at present.
|
|
extension: None,
|
|
exclude: options.exclude.map(|paths| {
|
|
paths
|
|
.into_iter()
|
|
.map(|pattern| {
|
|
let absolute = fs::normalize_path_to(&pattern, project_root);
|
|
FilePattern::User(pattern, absolute)
|
|
})
|
|
.collect()
|
|
}),
|
|
preview: options.preview.map(PreviewMode::from),
|
|
indent_style: options.indent_style,
|
|
quote_style: options.quote_style,
|
|
magic_trailing_comma: options.skip_magic_trailing_comma.map(|skip| {
|
|
if skip {
|
|
MagicTrailingComma::Ignore
|
|
} else {
|
|
MagicTrailingComma::Respect
|
|
}
|
|
}),
|
|
line_ending: options.line_ending,
|
|
docstring_code_format: options.docstring_code_format.map(|yes| {
|
|
if yes {
|
|
DocstringCode::Enabled
|
|
} else {
|
|
DocstringCode::Disabled
|
|
}
|
|
}),
|
|
docstring_code_line_width: options.docstring_code_line_length,
|
|
})
|
|
}
|
|
|
|
#[must_use]
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn combine(self, config: Self) -> Self {
|
|
Self {
|
|
exclude: self.exclude.or(config.exclude),
|
|
preview: self.preview.or(config.preview),
|
|
extension: self.extension.or(config.extension),
|
|
indent_style: self.indent_style.or(config.indent_style),
|
|
quote_style: self.quote_style.or(config.quote_style),
|
|
magic_trailing_comma: self.magic_trailing_comma.or(config.magic_trailing_comma),
|
|
line_ending: self.line_ending.or(config.line_ending),
|
|
docstring_code_format: self.docstring_code_format.or(config.docstring_code_format),
|
|
docstring_code_line_width: self
|
|
.docstring_code_line_width
|
|
.or(config.docstring_code_line_width),
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
fn warn_about_deprecated_top_level_lint_options(
|
|
top_level_options: &LintCommonOptions,
|
|
path: Option<&Path>,
|
|
) {
|
|
#[allow(deprecated)]
|
|
let LintCommonOptions {
|
|
allowed_confusables,
|
|
dummy_variable_rgx,
|
|
extend_ignore,
|
|
extend_select,
|
|
extend_fixable,
|
|
extend_unfixable,
|
|
external,
|
|
fixable,
|
|
ignore,
|
|
extend_safe_fixes,
|
|
extend_unsafe_fixes,
|
|
ignore_init_module_imports,
|
|
logger_objects,
|
|
select,
|
|
explicit_preview_rules,
|
|
task_tags,
|
|
typing_modules,
|
|
unfixable,
|
|
flake8_annotations,
|
|
flake8_bandit,
|
|
flake8_boolean_trap,
|
|
flake8_bugbear,
|
|
flake8_builtins,
|
|
flake8_comprehensions,
|
|
flake8_copyright,
|
|
flake8_errmsg,
|
|
flake8_quotes,
|
|
flake8_self,
|
|
flake8_tidy_imports,
|
|
flake8_type_checking,
|
|
flake8_gettext,
|
|
flake8_implicit_str_concat,
|
|
flake8_import_conventions,
|
|
flake8_pytest_style,
|
|
flake8_unused_arguments,
|
|
isort,
|
|
mccabe,
|
|
pep8_naming,
|
|
pycodestyle,
|
|
pydocstyle,
|
|
pyflakes,
|
|
pylint,
|
|
pyupgrade,
|
|
per_file_ignores,
|
|
extend_per_file_ignores,
|
|
} = top_level_options;
|
|
let mut used_options = Vec::new();
|
|
|
|
if allowed_confusables.is_some() {
|
|
used_options.push("allowed-confusables");
|
|
}
|
|
|
|
if dummy_variable_rgx.is_some() {
|
|
used_options.push("dummy-variable-rgx");
|
|
}
|
|
|
|
if extend_ignore.is_some() {
|
|
used_options.push("extend-ignore");
|
|
}
|
|
|
|
if extend_select.is_some() {
|
|
used_options.push("extend-select");
|
|
}
|
|
|
|
if extend_fixable.is_some() {
|
|
used_options.push("extend-fixable");
|
|
}
|
|
|
|
if extend_unfixable.is_some() {
|
|
used_options.push("extend-unfixable");
|
|
}
|
|
|
|
if external.is_some() {
|
|
used_options.push("external");
|
|
}
|
|
|
|
if fixable.is_some() {
|
|
used_options.push("fixable");
|
|
}
|
|
|
|
if ignore.is_some() {
|
|
used_options.push("ignore");
|
|
}
|
|
|
|
if extend_safe_fixes.is_some() {
|
|
used_options.push("extend-safe-fixes");
|
|
}
|
|
|
|
if extend_unsafe_fixes.is_some() {
|
|
used_options.push("extend-unsafe-fixes");
|
|
}
|
|
|
|
if ignore_init_module_imports.is_some() {
|
|
used_options.push("ignore-init-module-imports");
|
|
}
|
|
|
|
if logger_objects.is_some() {
|
|
used_options.push("logger-objects");
|
|
}
|
|
|
|
if select.is_some() {
|
|
used_options.push("select");
|
|
}
|
|
|
|
if explicit_preview_rules.is_some() {
|
|
used_options.push("explicit-preview-rules");
|
|
}
|
|
|
|
if task_tags.is_some() {
|
|
used_options.push("task-tags");
|
|
}
|
|
|
|
if typing_modules.is_some() {
|
|
used_options.push("typing-modules");
|
|
}
|
|
|
|
if unfixable.is_some() {
|
|
used_options.push("unfixable");
|
|
}
|
|
|
|
if flake8_annotations.is_some() {
|
|
used_options.push("flake8-annotations");
|
|
}
|
|
|
|
if flake8_bandit.is_some() {
|
|
used_options.push("flake8-bandit");
|
|
}
|
|
|
|
if flake8_boolean_trap.is_some() {
|
|
used_options.push("flake8-boolean-trap");
|
|
}
|
|
|
|
if flake8_bugbear.is_some() {
|
|
used_options.push("flake8-bugbear");
|
|
}
|
|
|
|
if flake8_builtins.is_some() {
|
|
used_options.push("flake8-builtins");
|
|
}
|
|
|
|
if flake8_comprehensions.is_some() {
|
|
used_options.push("flake8-comprehensions");
|
|
}
|
|
|
|
if flake8_copyright.is_some() {
|
|
used_options.push("flake8-copyright");
|
|
}
|
|
|
|
if flake8_errmsg.is_some() {
|
|
used_options.push("flake8-errmsg");
|
|
}
|
|
|
|
if flake8_quotes.is_some() {
|
|
used_options.push("flake8-quotes");
|
|
}
|
|
|
|
if flake8_self.is_some() {
|
|
used_options.push("flake8-self");
|
|
}
|
|
|
|
if flake8_tidy_imports.is_some() {
|
|
used_options.push("flake8-tidy-imports");
|
|
}
|
|
|
|
if flake8_type_checking.is_some() {
|
|
used_options.push("flake8-type-checking");
|
|
}
|
|
|
|
if flake8_gettext.is_some() {
|
|
used_options.push("flake8-gettext");
|
|
}
|
|
|
|
if flake8_implicit_str_concat.is_some() {
|
|
used_options.push("flake8-implicit-str-concat");
|
|
}
|
|
|
|
if flake8_import_conventions.is_some() {
|
|
used_options.push("flake8-import-conventions");
|
|
}
|
|
|
|
if flake8_pytest_style.is_some() {
|
|
used_options.push("flake8-pytest-style");
|
|
}
|
|
|
|
if flake8_unused_arguments.is_some() {
|
|
used_options.push("flake8-unused-arguments");
|
|
}
|
|
|
|
if isort.is_some() {
|
|
used_options.push("isort");
|
|
}
|
|
|
|
if mccabe.is_some() {
|
|
used_options.push("mccabe");
|
|
}
|
|
|
|
if pep8_naming.is_some() {
|
|
used_options.push("pep8-naming");
|
|
}
|
|
|
|
if pycodestyle.is_some() {
|
|
used_options.push("pycodestyle");
|
|
}
|
|
|
|
if pydocstyle.is_some() {
|
|
used_options.push("pydocstyle");
|
|
}
|
|
|
|
if pyflakes.is_some() {
|
|
used_options.push("pyflakes");
|
|
}
|
|
|
|
if pylint.is_some() {
|
|
used_options.push("pylint");
|
|
}
|
|
|
|
if pyupgrade.is_some() {
|
|
used_options.push("pyupgrade");
|
|
}
|
|
|
|
if per_file_ignores.is_some() {
|
|
used_options.push("per-file-ignores");
|
|
}
|
|
|
|
if extend_per_file_ignores.is_some() {
|
|
used_options.push("extend-per-file-ignores");
|
|
}
|
|
|
|
if used_options.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let options_mapping = used_options
|
|
.iter()
|
|
.map(|option| format!("- '{option}' -> 'lint.{option}'"))
|
|
.join("\n ");
|
|
|
|
let thing_to_update = path.map_or_else(
|
|
|| String::from("your `--config` CLI arguments"),
|
|
|path| format!("`{}`", fs::relativize_path(path)),
|
|
);
|
|
|
|
warn_user_once_by_message!(
|
|
"The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. \
|
|
Please update the following options in {thing_to_update}:\n {options_mapping}",
|
|
);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::str::FromStr;
|
|
|
|
use anyhow::Result;
|
|
|
|
use ruff_linter::codes::{Flake8Copyright, Pycodestyle, Refurb};
|
|
use ruff_linter::registry::{Linter, Rule, RuleSet};
|
|
use ruff_linter::rule_selector::PreviewOptions;
|
|
use ruff_linter::settings::types::PreviewMode;
|
|
use ruff_linter::RuleSelector;
|
|
|
|
use crate::configuration::{LintConfiguration, RuleSelection};
|
|
use crate::options::PydocstyleOptions;
|
|
|
|
const PREVIEW_RULES: &[Rule] = &[
|
|
Rule::ReimplementedStarmap,
|
|
Rule::SliceCopy,
|
|
Rule::TooManyPublicMethods,
|
|
Rule::TooManyPublicMethods,
|
|
Rule::UnnecessaryEnumerate,
|
|
Rule::MathConstant,
|
|
Rule::PreviewTestRule,
|
|
Rule::BlankLineBetweenMethods,
|
|
Rule::BlankLinesTopLevel,
|
|
Rule::TooManyBlankLines,
|
|
Rule::BlankLineAfterDecorator,
|
|
Rule::BlankLinesAfterFunctionOrClass,
|
|
Rule::BlankLinesBeforeNestedDefinition,
|
|
];
|
|
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
fn resolve_rules(
|
|
selections: impl IntoIterator<Item = RuleSelection>,
|
|
preview: Option<PreviewOptions>,
|
|
) -> Result<RuleSet> {
|
|
Ok(LintConfiguration {
|
|
rule_selections: selections.into_iter().collect(),
|
|
explicit_preview_rules: preview.as_ref().map(|preview| preview.require_explicit),
|
|
..LintConfiguration::default()
|
|
}
|
|
.as_rule_table(preview.map(|preview| preview.mode).unwrap_or_default())?
|
|
.iter_enabled()
|
|
.collect())
|
|
}
|
|
|
|
#[test]
|
|
fn select_linter() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Linter::Pycodestyle.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
None,
|
|
)?;
|
|
|
|
let expected = RuleSet::from_rules(&[
|
|
Rule::MixedSpacesAndTabs,
|
|
Rule::MultipleImportsOnOneLine,
|
|
Rule::ModuleImportNotAtTopOfFile,
|
|
Rule::LineTooLong,
|
|
Rule::MultipleStatementsOnOneLineColon,
|
|
Rule::MultipleStatementsOnOneLineSemicolon,
|
|
Rule::UselessSemicolon,
|
|
Rule::NoneComparison,
|
|
Rule::TrueFalseComparison,
|
|
Rule::NotInTest,
|
|
Rule::NotIsTest,
|
|
Rule::TypeComparison,
|
|
Rule::BareExcept,
|
|
Rule::LambdaAssignment,
|
|
Rule::AmbiguousVariableName,
|
|
Rule::AmbiguousClassName,
|
|
Rule::AmbiguousFunctionName,
|
|
Rule::IOError,
|
|
Rule::SyntaxError,
|
|
Rule::TabIndentation,
|
|
Rule::TrailingWhitespace,
|
|
Rule::MissingNewlineAtEndOfFile,
|
|
Rule::BlankLineWithWhitespace,
|
|
Rule::DocLineTooLong,
|
|
Rule::InvalidEscapeSequence,
|
|
]);
|
|
assert_eq!(actual, expected);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_one_char_prefix() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Pycodestyle::W.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
None,
|
|
)?;
|
|
|
|
let expected = RuleSet::from_rules(&[
|
|
Rule::TrailingWhitespace,
|
|
Rule::MissingNewlineAtEndOfFile,
|
|
Rule::BlankLineWithWhitespace,
|
|
Rule::DocLineTooLong,
|
|
Rule::InvalidEscapeSequence,
|
|
Rule::TabIndentation,
|
|
]);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_two_char_prefix() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Pycodestyle::W6.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::from_rule(Rule::InvalidEscapeSequence);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_prefix_ignore_code() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Pycodestyle::W.into()]),
|
|
ignore: vec![Pycodestyle::W292.into()],
|
|
..RuleSelection::default()
|
|
}],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::from_rules(&[
|
|
Rule::TrailingWhitespace,
|
|
Rule::BlankLineWithWhitespace,
|
|
Rule::DocLineTooLong,
|
|
Rule::InvalidEscapeSequence,
|
|
Rule::TabIndentation,
|
|
]);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_code_ignore_prefix() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Pycodestyle::W292.into()]),
|
|
ignore: vec![Pycodestyle::W.into()],
|
|
..RuleSelection::default()
|
|
}],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::from_rule(Rule::MissingNewlineAtEndOfFile);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_code_ignore_code() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Pycodestyle::W605.into()]),
|
|
ignore: vec![Pycodestyle::W605.into()],
|
|
..RuleSelection::default()
|
|
}],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::empty();
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_prefix_ignore_code_then_extend_select_code() -> Result<()> {
|
|
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()
|
|
},
|
|
],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::from_rules(&[
|
|
Rule::TrailingWhitespace,
|
|
Rule::MissingNewlineAtEndOfFile,
|
|
Rule::BlankLineWithWhitespace,
|
|
Rule::DocLineTooLong,
|
|
Rule::InvalidEscapeSequence,
|
|
Rule::TabIndentation,
|
|
]);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_prefix_ignore_code_then_extend_select_code_ignore_prefix() -> Result<()> {
|
|
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()
|
|
},
|
|
],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::from_rule(Rule::MissingNewlineAtEndOfFile);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn ignore_code_then_select_prefix() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[
|
|
RuleSelection {
|
|
select: Some(vec![]),
|
|
ignore: vec![Pycodestyle::W292.into()],
|
|
..RuleSelection::default()
|
|
},
|
|
RuleSelection {
|
|
select: Some(vec![Pycodestyle::W.into()]),
|
|
..RuleSelection::default()
|
|
},
|
|
],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::from_rules(&[
|
|
Rule::TrailingWhitespace,
|
|
Rule::BlankLineWithWhitespace,
|
|
Rule::DocLineTooLong,
|
|
Rule::InvalidEscapeSequence,
|
|
Rule::TabIndentation,
|
|
]);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn ignore_code_then_select_prefix_ignore_code() -> Result<()> {
|
|
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()
|
|
},
|
|
],
|
|
None,
|
|
)?;
|
|
let expected = RuleSet::from_rules(&[
|
|
Rule::TrailingWhitespace,
|
|
Rule::BlankLineWithWhitespace,
|
|
Rule::InvalidEscapeSequence,
|
|
Rule::TabIndentation,
|
|
]);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_all_preview() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![RuleSelector::All]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Disabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
assert!(!actual.intersects(&RuleSet::from_rules(PREVIEW_RULES)));
|
|
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![RuleSelector::All]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Enabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
assert!(actual.intersects(&RuleSet::from_rules(PREVIEW_RULES)));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_linter_preview() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Linter::Flake8Copyright.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Disabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
let expected = RuleSet::empty();
|
|
assert_eq!(actual, expected);
|
|
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Linter::Flake8Copyright.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Enabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
let expected = RuleSet::from_rule(Rule::MissingCopyrightNotice);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_prefix_preview() -> Result<()> {
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Flake8Copyright::_0.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Disabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
let expected = RuleSet::empty();
|
|
assert_eq!(actual, expected);
|
|
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Flake8Copyright::_0.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Enabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
let expected = RuleSet::from_rule(Rule::MissingCopyrightNotice);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_rule_preview() -> Result<()> {
|
|
// Test inclusion when toggling preview on and off
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Refurb::_145.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Disabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
let expected = RuleSet::empty();
|
|
assert_eq!(actual, expected);
|
|
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Refurb::_145.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Enabled,
|
|
..PreviewOptions::default()
|
|
}),
|
|
)?;
|
|
let expected = RuleSet::from_rule(Rule::SliceCopy);
|
|
assert_eq!(actual, expected);
|
|
|
|
// Test inclusion when preview is on but explicit codes are required
|
|
let actual = resolve_rules(
|
|
[RuleSelection {
|
|
select: Some(vec![Refurb::_145.into()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
Some(PreviewOptions {
|
|
mode: PreviewMode::Enabled,
|
|
require_explicit: true,
|
|
}),
|
|
)?;
|
|
let expected = RuleSet::from_rule(Rule::SliceCopy);
|
|
assert_eq!(actual, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn select_docstring_convention_override() -> Result<()> {
|
|
fn assert_override(
|
|
rule_selections: Vec<RuleSelection>,
|
|
should_be_overridden: bool,
|
|
) -> Result<()> {
|
|
use ruff_linter::rules::pydocstyle::settings::Convention;
|
|
|
|
let config = LintConfiguration {
|
|
rule_selections,
|
|
pydocstyle: Some(PydocstyleOptions {
|
|
convention: Some(Convention::Numpy),
|
|
..PydocstyleOptions::default()
|
|
}),
|
|
..LintConfiguration::default()
|
|
};
|
|
|
|
let mut expected = RuleSet::from_rules(&[
|
|
Rule::from_code("D410").unwrap(),
|
|
Rule::from_code("D411").unwrap(),
|
|
Rule::from_code("D412").unwrap(),
|
|
Rule::from_code("D414").unwrap(),
|
|
Rule::from_code("D418").unwrap(),
|
|
Rule::from_code("D419").unwrap(),
|
|
]);
|
|
if should_be_overridden {
|
|
expected.insert(Rule::from_code("D417").unwrap());
|
|
}
|
|
assert_eq!(
|
|
config
|
|
.as_rule_table(PreviewMode::Disabled)?
|
|
.iter_enabled()
|
|
.collect::<RuleSet>(),
|
|
expected,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
let d41 = RuleSelector::from_str("D41").unwrap();
|
|
let d417 = RuleSelector::from_str("D417").unwrap();
|
|
|
|
// D417 does not appear when D41 is provided...
|
|
assert_override(
|
|
vec![RuleSelection {
|
|
select: Some(vec![d41.clone()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
false,
|
|
)?;
|
|
|
|
// ...but does appear when specified directly.
|
|
assert_override(
|
|
vec![RuleSelection {
|
|
select: Some(vec![d41.clone(), d417.clone()]),
|
|
..RuleSelection::default()
|
|
}],
|
|
true,
|
|
)?;
|
|
|
|
// ...but disappears if there's a subsequent `--select`.
|
|
assert_override(
|
|
vec![
|
|
RuleSelection {
|
|
select: Some(vec![d417.clone()]),
|
|
..RuleSelection::default()
|
|
},
|
|
RuleSelection {
|
|
select: Some(vec![d41.clone()]),
|
|
..RuleSelection::default()
|
|
},
|
|
],
|
|
false,
|
|
)?;
|
|
|
|
// ...although an `--extend-select` is fine.
|
|
assert_override(
|
|
vec![
|
|
RuleSelection {
|
|
select: Some(vec![d417.clone()]),
|
|
..RuleSelection::default()
|
|
},
|
|
RuleSelection {
|
|
extend_select: vec![d41.clone()],
|
|
..RuleSelection::default()
|
|
},
|
|
],
|
|
true,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|