From 8f4daccf8b9b35c16863e1b9815f43cf0b355b44 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 08:44:08 -0500 Subject: [PATCH 01/33] add option --- crates/ruff/tests/lint.rs | 19 +++++++++++++++++++ crates/ruff_linter/src/settings/mod.rs | 4 +++- crates/ruff_workspace/src/configuration.rs | 13 +++++++++++++ crates/ruff_workspace/src/options.rs | 13 +++++++++++++ ruff.schema.json | 10 ++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index a082ab7cbb6c0..ad17c40c74215 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -2567,3 +2567,22 @@ fn a005_module_shadowing_strict_default() -> Result<()> { }); Ok(()) } + +#[test] +fn per_file_target_version_exists() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#]) + .args(["--select", "A005"]) // something that won't trigger + .arg("-") + .pass_stdin("1"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); +} diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 01f9f7220ff0b..6f0eaa2afa7a9 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -4,7 +4,7 @@ use path_absolutize::path_dedot; use regex::Regex; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; @@ -220,6 +220,7 @@ pub struct LinterSettings { pub fix_safety: FixSafetyTable, pub target_version: PythonVersion, + pub per_file_target_version: FxHashMap, pub preview: PreviewMode, pub explicit_preview_rules: bool, @@ -378,6 +379,7 @@ impl LinterSettings { Self { exclude: FilePatternSet::default(), target_version: PythonVersion::default(), + per_file_target_version: FxHashMap::default(), project_root: project_root.to_path_buf(), rules: DEFAULT_SELECTORS .iter() diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index f0c86431644b9..5418159a429d6 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -138,6 +138,7 @@ pub struct Configuration { pub namespace_packages: Option>, pub src: Option>, pub target_version: Option, + pub per_file_target_version: Option>, // Global formatting options pub line_length: Option, @@ -279,6 +280,7 @@ impl Configuration { extension: self.extension.unwrap_or_default(), preview: lint_preview, target_version, + per_file_target_version: self.per_file_target_version.unwrap_or_default(), project_root: project_root.to_path_buf(), allowed_confusables: lint .allowed_confusables @@ -452,6 +454,13 @@ impl Configuration { } }; + let per_file_target_version = options.per_file_target_version.map(|versions| { + versions + .into_iter() + .map(|(glob, version)| (glob, ast::PythonVersion::from(version))) + .collect() + }); + Ok(Self { builtins: options.builtins, cache_dir: options @@ -533,6 +542,7 @@ impl Configuration { .map(|src| resolve_src(&src, project_root)) .transpose()?, target_version: options.target_version.map(ast::PythonVersion::from), + per_file_target_version, // `--extension` is a hidden command-line argument that isn't supported in configuration // files at present. extension: None, @@ -580,6 +590,9 @@ impl Configuration { show_fixes: self.show_fixes.or(config.show_fixes), src: self.src.or(config.src), target_version: self.target_version.or(config.target_version), + per_file_target_version: self + .per_file_target_version + .or(config.per_file_target_version), preview: self.preview.or(config.preview), extension: self.extension.or(config.extension), diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 88994f6ecccbe..7b258c9ccf9a4 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -333,6 +333,19 @@ pub struct Options { )] pub target_version: Option, + /// A list of mappings from file pattern to Python version to use when checking the + /// corresponding file(s). + #[option( + default = "{}", + value_type = "dict[str, PythonVersion]", + scope = "per-file-target-version", + example = r#" + # Override the project-wide Python version for a developer scripts directory: + "scripts/**.py" = "py312" + "# + )] + pub per_file_target_version: Option>, + /// The directories to consider when resolving first- vs. third-party /// imports. /// diff --git a/ruff.schema.json b/ruff.schema.json index 9cac3857adff0..57dd60a7c45f0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -578,6 +578,16 @@ } } }, + "per-file-target-version": { + "description": "A list of mappings from glob-style file pattern to Python version to use when checking the corresponding file(s).\n\nThis may be useful for overriding the global Python version settings in `target-version` or `requires-python` for a subset of files. For example, if you have a project with a minimum supported Python version of 3.9 but a subdirectory of developer scripts that want to use a newer feature like the `match` statement from Python 3.10, you can use `per-file-target-version` to specify `\"developer_scripts/*.py\" = \"py310\"`.\n\nThis setting is used by the linter to enforce any enabled version-specific lint rules, as well as by the formatter for any version-specific formatting options, such as parenthesizing context managers on Python 3.10+.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/PythonVersion" + } + }, "preview": { "description": "Whether to enable preview mode. When preview mode is enabled, Ruff will use unstable rules, fixes, and formatting.", "type": [ From 377b6bd78fd61414444e625073911ce856239a71 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 09:32:23 -0500 Subject: [PATCH 02/33] use compiled patterns for resolving --- crates/ruff_linter/src/settings/mod.rs | 21 ++++++++++-- crates/ruff_linter/src/settings/types.rs | 37 ++++++++++++++++++++++ crates/ruff_workspace/src/configuration.rs | 8 +++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 6f0eaa2afa7a9..d0f2d495e70f5 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -4,10 +4,11 @@ use path_absolutize::path_dedot; use regex::Regex; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; +use types::{CompiledPerFileVersion, CompiledPerFileVersionList}; use crate::codes::RuleCodePrefix; use ruff_macros::CacheKey; @@ -220,7 +221,7 @@ pub struct LinterSettings { pub fix_safety: FixSafetyTable, pub target_version: PythonVersion, - pub per_file_target_version: FxHashMap, + pub per_file_target_version: CompiledPerFileVersionList, pub preview: PreviewMode, pub explicit_preview_rules: bool, @@ -379,7 +380,7 @@ impl LinterSettings { Self { exclude: FilePatternSet::default(), target_version: PythonVersion::default(), - per_file_target_version: FxHashMap::default(), + per_file_target_version: CompiledPerFileVersionList::default(), project_root: project_root.to_path_buf(), rules: DEFAULT_SELECTORS .iter() @@ -444,6 +445,20 @@ impl LinterSettings { self.target_version = target_version; self } + + /// Resolve the [`PythonVersion`] to use for linting. + /// + /// This method respects the per-file version overrides in + /// [`LinterSettings::per_file_target_version`] and falls back on + /// [`LinterSettings::unresolved_target_version`] if none of the override patterns match. + pub fn resolve_target_version(&self, path: &Path) -> PythonVersion { + for CompiledPerFileVersion { matcher, version } in &*self.per_file_target_version { + if matcher.is_match(path) { + return *version; + } + } + self.target_version + } } impl Default for LinterSettings { diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 007a4a4e2bc24..8d757f9b8aa3e 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -635,6 +635,43 @@ impl Deref for CompiledPerFileIgnoreList { } } +#[derive(Debug, Clone, CacheKey)] +pub struct CompiledPerFileVersion { + pub matcher: GlobMatcher, + pub version: ast::PythonVersion, +} + +#[derive(CacheKey, Clone, Debug, Default)] +pub struct CompiledPerFileVersionList { + versions: Vec, +} + +impl CompiledPerFileVersionList { + /// Given a list of patterns, create a `GlobSet`. + pub fn resolve(per_file_ignores: FxHashMap) -> Result { + let versions: Result> = per_file_ignores + .into_iter() + .map(|(pattern, version)| { + Ok(CompiledPerFileVersion { + matcher: Glob::new(&pattern)?.compile_matcher(), + version, + }) + }) + .collect(); + Ok(Self { + versions: versions?, + }) + } +} + +impl Deref for CompiledPerFileVersionList { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.versions + } +} + #[cfg(test)] mod tests { #[test] diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 5418159a429d6..898c72febe270 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -29,8 +29,8 @@ use ruff_linter::rules::{flake8_import_conventions, isort, 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, RequiredVersion, UnsafeFixes, + CompiledPerFileIgnoreList, CompiledPerFileVersionList, ExtensionMapping, FilePattern, + FilePatternSet, OutputFormat, PerFileIgnore, PreviewMode, RequiredVersion, UnsafeFixes, }; use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS}; use ruff_linter::{ @@ -280,7 +280,9 @@ impl Configuration { extension: self.extension.unwrap_or_default(), preview: lint_preview, target_version, - per_file_target_version: self.per_file_target_version.unwrap_or_default(), + per_file_target_version: CompiledPerFileVersionList::resolve( + self.per_file_target_version.unwrap_or_default(), + )?, project_root: project_root.to_path_buf(), allowed_confusables: lint .allowed_confusables From b418e31ca6904ead089890bac7ba8841955f9248 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 12:19:09 -0500 Subject: [PATCH 03/33] use Checker::target_version, LinterSettings::resolve_target_version --- .../src/checkers/ast/analyze/expression.rs | 40 +++++++++---------- .../src/checkers/ast/analyze/statement.rs | 18 ++++----- crates/ruff_linter/src/checkers/ast/mod.rs | 16 +++++--- crates/ruff_linter/src/checkers/imports.rs | 4 +- crates/ruff_linter/src/linter.rs | 3 ++ crates/ruff_linter/src/renamer.rs | 2 +- .../rules/fastapi_non_annotated_dependency.rs | 4 +- .../flake8_annotations/rules/definition.rs | 12 +++--- .../rules/async_function_with_timeout.rs | 2 +- .../rules/batched_without_explicit_strict.rs | 2 +- .../rules/class_as_data_structure.rs | 2 +- .../rules/builtin_argument_shadowing.rs | 2 +- .../rules/builtin_attribute_shadowing.rs | 2 +- .../rules/builtin_import_shadowing.rs | 2 +- .../builtin_lambda_argument_shadowing.rs | 2 +- .../rules/builtin_variable_shadowing.rs | 2 +- .../rules/stdlib_module_shadowing.rs | 10 +++-- .../rules/custom_type_var_for_self.rs | 2 +- .../rules/no_return_argument_annotation.rs | 2 +- .../flake8_pyi/rules/non_self_return_type.rs | 2 +- .../rules/pre_pep570_positional_argument.rs | 2 +- .../rules/redundant_none_literal.rs | 4 +- .../rules/flake8_pyi/rules/simple_defaults.rs | 2 +- .../rules/avoidable_escaped_quote.rs | 16 ++++++-- .../rules/type_alias_quotes.rs | 23 +++++------ .../rules/typing_only_runtime_import.rs | 2 +- .../rules/replaceable_by_pathlib.rs | 2 +- .../src/rules/isort/rules/organize_imports.rs | 5 ++- .../perflint/rules/try_except_in_loop.rs | 2 +- .../src/rules/pyflakes/rules/unused_import.rs | 2 +- .../rules/pylint/rules/bad_str_strip_call.rs | 2 +- .../pylint/rules/unnecessary_dunder_call.rs | 2 +- .../rules/useless_exception_statement.rs | 2 +- .../pyupgrade/rules/deprecated_import.rs | 2 +- .../pyupgrade/rules/outdated_version_block.rs | 2 +- .../rules/pep695/non_pep695_generic_class.rs | 2 +- .../pep695/non_pep695_generic_function.rs | 2 +- .../rules/pep695/non_pep695_type_alias.rs | 4 +- .../pyupgrade/rules/timeout_error_alias.rs | 8 ++-- .../pyupgrade/rules/use_pep585_annotation.rs | 4 +- .../pyupgrade/rules/use_pep604_annotation.rs | 2 +- .../pyupgrade/rules/use_pep646_unpack.rs | 2 +- .../src/rules/refurb/rules/bit_count.rs | 2 +- .../refurb/rules/fromisoformat_replace_z.rs | 2 +- .../src/rules/refurb/rules/read_whole_file.rs | 7 +--- .../rules/slice_to_remove_prefix_or_suffix.rs | 4 +- .../rules/refurb/rules/write_whole_file.rs | 7 +--- .../ruff/rules/class_with_mixed_type_vars.rs | 2 +- .../src/rules/ruff/rules/implicit_optional.rs | 14 +++---- ...rectly_parenthesized_tuple_in_subscript.rs | 2 +- crates/ruff_linter/src/settings/mod.rs | 11 ++--- crates/ruff_linter/src/settings/types.rs | 6 +++ 52 files changed, 145 insertions(+), 137 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 7795d8c1ac32b..3c386f7ea07ba 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -34,8 +34,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { { if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() - && checker.settings.target_version < PythonVersion::PY310 - && checker.settings.target_version >= PythonVersion::PY37 + && checker.target_version() < PythonVersion::PY310 + && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() && !checker.settings.pyupgrade.keep_runtime_typing { @@ -49,8 +49,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { Rule::NonPEP604AnnotationOptional, ]) { if checker.source_type.is_stub() - || checker.settings.target_version >= PythonVersion::PY310 - || (checker.settings.target_version >= PythonVersion::PY37 + || checker.target_version() >= PythonVersion::PY310 + || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.future_annotations_or_stub() && checker.semantic.in_annotation() && !checker.settings.pyupgrade.keep_runtime_typing) @@ -64,7 +64,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { // Ex) list[...] if checker.enabled(Rule::FutureRequiredTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() - && checker.settings.target_version < PythonVersion::PY39 + && checker.target_version() < PythonVersion::PY39 && checker.semantic.in_annotation() && checker.semantic.in_runtime_evaluated_annotation() && !checker.semantic.in_string_type_definition() @@ -135,7 +135,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } if checker.enabled(Rule::UnnecessaryDefaultTypeArgs) { - if checker.settings.target_version >= PythonVersion::PY313 { + if checker.target_version() >= PythonVersion::PY313 { pyupgrade::rules::unnecessary_default_type_args(checker, expr); } } @@ -268,8 +268,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { { if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() - && checker.settings.target_version < PythonVersion::PY39 - && checker.settings.target_version >= PythonVersion::PY37 + && checker.target_version() < PythonVersion::PY39 + && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() && !checker.settings.pyupgrade.keep_runtime_typing { @@ -278,8 +278,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } if checker.enabled(Rule::NonPEP585Annotation) { if checker.source_type.is_stub() - || checker.settings.target_version >= PythonVersion::PY39 - || (checker.settings.target_version >= PythonVersion::PY37 + || checker.target_version() >= PythonVersion::PY39 + || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.future_annotations_or_stub() && checker.semantic.in_annotation() && !checker.settings.pyupgrade.keep_runtime_typing) @@ -378,8 +378,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() - && checker.settings.target_version < PythonVersion::PY39 - && checker.settings.target_version >= PythonVersion::PY37 + && checker.target_version() < PythonVersion::PY39 + && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() && !checker.settings.pyupgrade.keep_runtime_typing { @@ -390,8 +390,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } if checker.enabled(Rule::NonPEP585Annotation) { if checker.source_type.is_stub() - || checker.settings.target_version >= PythonVersion::PY39 - || (checker.settings.target_version >= PythonVersion::PY37 + || checker.target_version() >= PythonVersion::PY39 + || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.future_annotations_or_stub() && checker.semantic.in_annotation() && !checker.settings.pyupgrade.keep_runtime_typing) @@ -405,7 +405,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { refurb::rules::regex_flag_alias(checker, expr); } if checker.enabled(Rule::DatetimeTimezoneUTC) { - if checker.settings.target_version >= PythonVersion::PY311 { + if checker.target_version() >= PythonVersion::PY311 { pyupgrade::rules::datetime_utc_alias(checker, expr); } } @@ -610,12 +610,12 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { pyupgrade::rules::os_error_alias_call(checker, func); } if checker.enabled(Rule::TimeoutErrorAlias) { - if checker.settings.target_version >= PythonVersion::PY310 { + if checker.target_version() >= PythonVersion::PY310 { pyupgrade::rules::timeout_error_alias_call(checker, func); } } if checker.enabled(Rule::NonPEP604Isinstance) { - if checker.settings.target_version >= PythonVersion::PY310 { + if checker.target_version() >= PythonVersion::PY310 { pyupgrade::rules::use_pep604_isinstance(checker, expr, func, args); } } @@ -690,7 +690,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ); } if checker.enabled(Rule::ZipWithoutExplicitStrict) { - if checker.settings.target_version >= PythonVersion::PY310 { + if checker.target_version() >= PythonVersion::PY310 { flake8_bugbear::rules::zip_without_explicit_strict(checker, call); } } @@ -963,7 +963,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { flake8_pytest_style::rules::fail_call(checker, call); } if checker.enabled(Rule::ZipInsteadOfPairwise) { - if checker.settings.target_version >= PythonVersion::PY310 { + if checker.target_version() >= PythonVersion::PY310 { ruff::rules::zip_instead_of_pairwise(checker, call); } } @@ -1385,7 +1385,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { // Ex) `str | None` if checker.enabled(Rule::FutureRequiredTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() - && checker.settings.target_version < PythonVersion::PY310 + && checker.target_version() < PythonVersion::PY310 && checker.semantic.in_annotation() && checker.semantic.in_runtime_evaluated_annotation() && !checker.semantic.in_string_type_definition() diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 4b5594e47f801..77863086d6989 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -164,9 +164,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt); } } - if checker.source_type.is_stub() - || checker.settings.target_version >= PythonVersion::PY311 - { + if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY311 { if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) { flake8_pyi::rules::no_return_argument_annotation(checker, parameters); } @@ -194,12 +192,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pylint::rules::global_statement(checker, name); } if checker.enabled(Rule::LRUCacheWithoutParameters) { - if checker.settings.target_version >= PythonVersion::PY38 { + if checker.target_version() >= PythonVersion::PY38 { pyupgrade::rules::lru_cache_without_parameters(checker, decorator_list); } } if checker.enabled(Rule::LRUCacheWithMaxsizeNone) { - if checker.settings.target_version >= PythonVersion::PY39 { + if checker.target_version() >= PythonVersion::PY39 { pyupgrade::rules::lru_cache_with_maxsize_none(checker, decorator_list); } } @@ -445,7 +443,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pyupgrade::rules::useless_object_inheritance(checker, class_def); } if checker.enabled(Rule::ReplaceStrEnum) { - if checker.settings.target_version >= PythonVersion::PY311 { + if checker.target_version() >= PythonVersion::PY311 { pyupgrade::rules::replace_str_enum(checker, class_def); } } @@ -765,7 +763,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } if checker.enabled(Rule::UnnecessaryFutureImport) { - if checker.settings.target_version >= PythonVersion::PY37 { + if checker.target_version() >= PythonVersion::PY37 { if let Some("__future__") = module { pyupgrade::rules::unnecessary_future_import(checker, stmt, names); } @@ -1039,7 +1037,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } if checker.enabled(Rule::TimeoutErrorAlias) { - if checker.settings.target_version >= PythonVersion::PY310 { + if checker.target_version() >= PythonVersion::PY310 { if let Some(item) = exc { pyupgrade::rules::timeout_error_alias_raise(checker, item); } @@ -1431,7 +1429,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody); } if checker.enabled(Rule::ContinueInFinally) { - if checker.settings.target_version <= PythonVersion::PY38 { + if checker.target_version() <= PythonVersion::PY38 { pylint::rules::continue_in_finally(checker, finalbody); } } @@ -1455,7 +1453,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pyupgrade::rules::os_error_alias_handlers(checker, handlers); } if checker.enabled(Rule::TimeoutErrorAlias) { - if checker.settings.target_version >= PythonVersion::PY310 { + if checker.target_version() >= PythonVersion::PY310 { pyupgrade::rules::timeout_error_alias_handlers(checker, handlers); } } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index e09405250fcf8..67b3bf40b5393 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -38,7 +38,7 @@ use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor}; use ruff_python_ast::{ self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern, - Stmt, Suite, UnaryOp, + PythonVersion, Stmt, Suite, UnaryOp, }; use ruff_python_ast::{helpers, str, visitor, PySourceType}; use ruff_python_codegen::{Generator, Stylist}; @@ -242,6 +242,7 @@ impl<'a> Checker<'a> { source_type: PySourceType, cell_offsets: Option<&'a CellOffsets>, notebook_index: Option<&'a NotebookIndex>, + target_version: PythonVersion, ) -> Checker<'a> { let mut semantic = SemanticModel::new(&settings.typing_modules, path, module); if settings.preview.is_enabled() { @@ -500,6 +501,10 @@ impl<'a> Checker<'a> { self.report_diagnostic(diagnostic); } } + + pub(crate) fn target_version(&self) -> PythonVersion { + self.settings.resolve_target_version(self.path) + } } impl<'a> Visitor<'a> for Checker<'a> { @@ -2108,17 +2113,14 @@ impl<'a> Checker<'a> { } fn bind_builtins(&mut self) { + let target_version = self.target_version(); let mut bind_builtin = |builtin| { // Add the builtin to the scope. let binding_id = self.semantic.push_builtin(); let scope = self.semantic.global_scope_mut(); scope.add(builtin, binding_id); }; - - let standard_builtins = python_builtins( - self.settings.target_version.minor, - self.source_type.is_ipynb(), - ); + let standard_builtins = python_builtins(target_version.minor, self.source_type.is_ipynb()); for builtin in standard_builtins { bind_builtin(builtin); } @@ -2664,6 +2666,7 @@ pub(crate) fn check_ast( source_type: PySourceType, cell_offsets: Option<&CellOffsets>, notebook_index: Option<&NotebookIndex>, + target_version: PythonVersion, ) -> Vec { let module_path = package .map(PackageRoot::path) @@ -2703,6 +2706,7 @@ pub(crate) fn check_ast( source_type, cell_offsets, notebook_index, + target_version, ); checker.bind_builtins(); diff --git a/crates/ruff_linter/src/checkers/imports.rs b/crates/ruff_linter/src/checkers/imports.rs index 7b3f1462c2ed2..2dd7a33a29999 100644 --- a/crates/ruff_linter/src/checkers/imports.rs +++ b/crates/ruff_linter/src/checkers/imports.rs @@ -3,7 +3,7 @@ use ruff_diagnostics::Diagnostic; use ruff_notebook::CellOffsets; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_ast::{ModModule, PySourceType}; +use ruff_python_ast::{ModModule, PySourceType, PythonVersion}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::Parsed; @@ -27,6 +27,7 @@ pub(crate) fn check_imports( package: Option>, source_type: PySourceType, cell_offsets: Option<&CellOffsets>, + target_version: PythonVersion, ) -> Vec { // Extract all import blocks from the AST. let tracker = { @@ -52,6 +53,7 @@ pub(crate) fn check_imports( package, source_type, parsed.tokens(), + target_version, ) { diagnostics.push(diagnostic); } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index ed27a6fdfd456..6c05ebdcd62b4 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -144,6 +144,7 @@ pub fn check_path( if use_ast || use_imports || use_doc_lines { let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets); let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index); + let target_version = settings.resolve_target_version(path); if use_ast { diagnostics.extend(check_ast( parsed, @@ -158,6 +159,7 @@ pub fn check_path( source_type, cell_offsets, notebook_index, + target_version, )); } if use_imports { @@ -171,6 +173,7 @@ pub fn check_path( package, source_type, cell_offsets, + target_version, ); diagnostics.extend(import_diagnostics); diff --git a/crates/ruff_linter/src/renamer.rs b/crates/ruff_linter/src/renamer.rs index 1b6c5835ec782..abf3434cde76f 100644 --- a/crates/ruff_linter/src/renamer.rs +++ b/crates/ruff_linter/src/renamer.rs @@ -399,7 +399,7 @@ impl ShadowedKind { if is_python_builtin( new_name, - checker.settings.target_version.minor, + checker.target_version().minor, checker.source_type.is_ipynb(), ) { return ShadowedKind::BuiltIn; diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index b393816140316..ec0e5e33a1ae7 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs @@ -226,13 +226,13 @@ fn create_diagnostic( ) -> bool { let mut diagnostic = Diagnostic::new( FastApiNonAnnotatedDependency { - py_version: checker.settings.target_version, + py_version: checker.target_version(), }, parameter.range, ); let try_generate_fix = || { - let module = if checker.settings.target_version >= PythonVersion::PY39 { + let module = if checker.target_version() >= PythonVersion::PY39 { "typing" } else { "typing_extensions" diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index e637f0ea131fe..56942616e7ea2 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -523,7 +523,7 @@ fn check_dynamically_typed( if type_hint_resolves_to_any( parsed_annotation.expression(), checker, - checker.settings.target_version, + checker.target_version(), ) { diagnostics.push(Diagnostic::new( AnyType { name: func() }, @@ -532,7 +532,7 @@ fn check_dynamically_typed( } } } else { - if type_hint_resolves_to_any(annotation, checker, checker.settings.target_version) { + if type_hint_resolves_to_any(annotation, checker, checker.target_version()) { diagnostics.push(Diagnostic::new( AnyType { name: func() }, annotation.range(), @@ -725,7 +725,7 @@ pub(crate) fn definition( checker.importer(), function.parameters.start(), checker.semantic(), - checker.settings.target_version, + checker.target_version(), ) }) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) @@ -756,7 +756,7 @@ pub(crate) fn definition( checker.importer(), function.parameters.start(), checker.semantic(), - checker.settings.target_version, + checker.target_version(), ) }) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) @@ -826,7 +826,7 @@ pub(crate) fn definition( checker.importer(), function.parameters.start(), checker.semantic(), - checker.settings.target_version, + checker.target_version(), ) }) .map(|(return_type, edits)| { @@ -865,7 +865,7 @@ pub(crate) fn definition( checker.importer(), function.parameters.start(), checker.semantic(), - checker.settings.target_version, + checker.target_version(), ) }) .map(|(return_type, edits)| { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs index b588cf9e2155a..53cbf952b160d 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs @@ -108,7 +108,7 @@ pub(crate) fn async_function_with_timeout(checker: &Checker, function_def: &ast: }; // asyncio.timeout feature was first introduced in Python 3.11 - if module == AsyncModule::AsyncIo && checker.settings.target_version < PythonVersion::PY311 { + if module == AsyncModule::AsyncIo && checker.target_version() < PythonVersion::PY311 { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs index 8b12f6e9ee823..0fcdb5c8d0321 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs @@ -59,7 +59,7 @@ impl Violation for BatchedWithoutExplicitStrict { /// B911 pub(crate) fn batched_without_explicit_strict(checker: &Checker, call: &ExprCall) { - if checker.settings.target_version < PythonVersion::PY313 { + if checker.target_version() < PythonVersion::PY313 { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs index 774a25b6da5e2..01bbc446e7283 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs @@ -78,7 +78,7 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl // skip `self` .skip(1) .all(|param| param.annotation().is_some() && !param.is_variadic()) - && (func_def.parameters.kwonlyargs.is_empty() || checker.settings.target_version >= PythonVersion::PY310) + && (func_def.parameters.kwonlyargs.is_empty() || checker.target_version() >= PythonVersion::PY310) // `__init__` should not have complicated logic in it // only assignments && func_def diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs index 8683222d02361..b0d5014de962e 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs @@ -68,7 +68,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &Checker, parameter: &Paramete parameter.name(), checker.source_type, &checker.settings.flake8_builtins.builtins_ignorelist, - checker.settings.target_version, + checker.target_version(), ) { // Ignore parameters in lambda expressions. // (That is the domain of A006.) diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index c151c117ca3ab..79f0f0ded34ee 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -99,7 +99,7 @@ pub(crate) fn builtin_attribute_shadowing( name, checker.source_type, &checker.settings.flake8_builtins.builtins_ignorelist, - checker.settings.target_version, + checker.target_version(), ) { // Ignore explicit overrides. if class_def.decorator_list.iter().any(|decorator| { diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs index bdebcbf035106..83332397b4712 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs @@ -61,7 +61,7 @@ pub(crate) fn builtin_import_shadowing(checker: &Checker, alias: &Alias) { name.as_str(), checker.source_type, &checker.settings.flake8_builtins.builtins_ignorelist, - checker.settings.target_version, + checker.target_version(), ) { checker.report_diagnostic(Diagnostic::new( BuiltinImportShadowing { diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs index 28d8461ed5845..1e778fa0cea06 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs @@ -44,7 +44,7 @@ pub(crate) fn builtin_lambda_argument_shadowing(checker: &Checker, lambda: &Expr name, checker.source_type, &checker.settings.flake8_builtins.builtins_ignorelist, - checker.settings.target_version, + checker.target_version(), ) { checker.report_diagnostic(Diagnostic::new( BuiltinLambdaArgumentShadowing { diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs index ecd919327ca39..b77b3e0ee64a1 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs @@ -63,7 +63,7 @@ pub(crate) fn builtin_variable_shadowing(checker: &Checker, name: &str, range: T name, checker.source_type, &checker.settings.flake8_builtins.builtins_ignorelist, - checker.settings.target_version, + checker.target_version(), ) { checker.report_diagnostic(Diagnostic::new( BuiltinVariableShadowing { diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs index a74d4c948d7ae..4a1e154cad337 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs @@ -98,7 +98,11 @@ pub(crate) fn stdlib_module_shadowing( let module_name = components.next()?; - if is_allowed_module(settings, &module_name) { + if is_allowed_module( + settings, + settings.resolve_target_version(&path).minor, + &module_name, + ) { return None; } @@ -129,7 +133,7 @@ fn get_prefix<'a>(settings: &'a LinterSettings, path: &Path) -> Option<&'a PathB prefix } -fn is_allowed_module(settings: &LinterSettings, module: &str) -> bool { +fn is_allowed_module(settings: &LinterSettings, minor_version: u8, module: &str) -> bool { // Shadowing private stdlib modules is okay. // https://github.com/astral-sh/ruff/issues/12949 if module.starts_with('_') && !module.starts_with("__") { @@ -145,5 +149,5 @@ fn is_allowed_module(settings: &LinterSettings, module: &str) -> bool { return true; } - !is_known_standard_library(settings.target_version.minor, module) + !is_known_standard_library(minor_version, module) } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index a3de8474372c9..8d89b29a82c20 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -560,7 +560,7 @@ fn replace_custom_typevar_with_self( /// This is because it was added to the `typing` module on Python 3.11, /// but is available from the backport package `typing_extensions` on all versions. fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String), ResolutionError> { - let source_module = if checker.settings.target_version >= PythonVersion::PY311 { + let source_module = if checker.target_version() >= PythonVersion::PY311 { "typing" } else { "typing_extensions" diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index 993419a49fdbf..5500f706a39eb 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -67,7 +67,7 @@ pub(crate) fn no_return_argument_annotation(checker: &Checker, parameters: &ast: if is_no_return(annotation, checker) { checker.report_diagnostic(Diagnostic::new( NoReturnArgumentAnnotationInStub { - module: if checker.settings.target_version >= PythonVersion::PY311 { + module: if checker.target_version() >= PythonVersion::PY311 { TypingModule::Typing } else { TypingModule::TypingExtensions diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 4364a9d4f919d..1675922077b4f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -215,7 +215,7 @@ fn replace_with_self_fix( let semantic = checker.semantic(); let (self_import, self_binding) = { - let source_module = if checker.settings.target_version >= PythonVersion::PY311 { + let source_module = if checker.target_version() >= PythonVersion::PY311 { "typing" } else { "typing_extensions" diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs index c1427deec6235..46970f64f8a13 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs @@ -56,7 +56,7 @@ impl Violation for Pep484StylePositionalOnlyParameter { /// PYI063 pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast::StmtFunctionDef) { // PEP 570 was introduced in Python 3.8. - if checker.settings.target_version < PythonVersion::PY38 { + if checker.target_version() < PythonVersion::PY38 { return; } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index b9e279e5e8590..9432521da6b47 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -112,9 +112,7 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex let union_kind = if literal_elements.is_empty() { UnionKind::NoUnion - } else if (checker.settings.target_version >= PythonVersion::PY310) - || checker.source_type.is_stub() - { + } else if (checker.target_version() >= PythonVersion::PY310) || checker.source_type.is_stub() { UnionKind::BitOr } else { UnionKind::TypingOptional diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index 2a3ab2f6f014c..3f0577928f7d2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -667,7 +667,7 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar return; } - let module = if checker.settings.target_version >= PythonVersion::PY310 { + let module = if checker.target_version() >= PythonVersion::PY310 { TypingModule::Typing } else { TypingModule::TypingExtensions diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs index 55df17fd4f805..91ec9c16b2d9e 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs @@ -3,7 +3,7 @@ use flake8_quotes::settings::Quote; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::visitor::{walk_f_string, Visitor}; -use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike}; +use ruff_python_ast::{self as ast, AnyStringFlags, PythonVersion, StringFlags, StringLike}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -61,7 +61,11 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike return; } - let mut rule_checker = AvoidableEscapedQuoteChecker::new(checker.locator(), checker.settings); + let mut rule_checker = AvoidableEscapedQuoteChecker::new( + checker.locator(), + checker.settings, + checker.target_version(), + ); for part in string_like.parts() { match part { @@ -88,11 +92,15 @@ struct AvoidableEscapedQuoteChecker<'a> { } impl<'a> AvoidableEscapedQuoteChecker<'a> { - fn new(locator: &'a Locator<'a>, settings: &'a LinterSettings) -> Self { + fn new( + locator: &'a Locator<'a>, + settings: &'a LinterSettings, + target_version: PythonVersion, + ) -> Self { Self { locator, quotes_settings: &settings.flake8_quotes, - supports_pep701: settings.target_version.supports_pep_701(), + supports_pep701: target_version.supports_pep_701(), diagnostics: vec![], } } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index c8f80c500a187..245741b9e9efb 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -10,7 +10,6 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::flake8_type_checking::helpers::quote_type_expression; -use crate::settings::LinterSettings; use ruff_python_ast::PythonVersion; /// ## What it does @@ -284,7 +283,7 @@ pub(crate) fn quoted_type_alias( // explicit type aliases require some additional checks to avoid false positives if checker.semantic().in_annotated_type_alias_value() - && quotes_are_unremovable(checker.semantic(), expr, checker.settings) + && quotes_are_unremovable(checker.semantic(), expr, checker.target_version()) { return; } @@ -305,7 +304,7 @@ pub(crate) fn quoted_type_alias( fn quotes_are_unremovable( semantic: &SemanticModel, expr: &Expr, - settings: &LinterSettings, + target_version: PythonVersion, ) -> bool { match expr { Expr::BinOp(ast::ExprBinOp { @@ -313,11 +312,11 @@ fn quotes_are_unremovable( }) => { match op { Operator::BitOr => { - if settings.target_version < PythonVersion::PY310 { + if target_version < PythonVersion::PY310 { return true; } - quotes_are_unremovable(semantic, left, settings) - || quotes_are_unremovable(semantic, right, settings) + quotes_are_unremovable(semantic, left, target_version) + || quotes_are_unremovable(semantic, right, target_version) } // for now we'll treat uses of other operators as unremovable quotes // since that would make it an invalid type expression anyways. We skip @@ -330,7 +329,7 @@ fn quotes_are_unremovable( value, ctx: ExprContext::Load, .. - }) => quotes_are_unremovable(semantic, value, settings), + }) => quotes_are_unremovable(semantic, value, target_version), Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { // for subscripts we don't know whether it's safe to do at runtime // since the operation may only be available at type checking time. @@ -338,7 +337,7 @@ fn quotes_are_unremovable( if !semantic.in_type_checking_block() { return true; } - if quotes_are_unremovable(semantic, value, settings) { + if quotes_are_unremovable(semantic, value, target_version) { return true; } // for `typing.Annotated`, only analyze the first argument, since the rest may @@ -347,23 +346,23 @@ fn quotes_are_unremovable( if semantic.match_typing_qualified_name(&qualified_name, "Annotated") { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { return !elts.is_empty() - && quotes_are_unremovable(semantic, &elts[0], settings); + && quotes_are_unremovable(semantic, &elts[0], target_version); } return false; } } - quotes_are_unremovable(semantic, slice, settings) + quotes_are_unremovable(semantic, slice, target_version) } Expr::Attribute(ast::ExprAttribute { value, .. }) => { // for attributes we also don't know whether it's safe if !semantic.in_type_checking_block() { return true; } - quotes_are_unremovable(semantic, value, settings) + quotes_are_unremovable(semantic, value, target_version) } Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { for elt in elts { - if quotes_are_unremovable(semantic, elt, settings) { + if quotes_are_unremovable(semantic, elt, target_version) { return true; } } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index ad873a8534d46..777a7b123713b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -307,7 +307,7 @@ pub(crate) fn typing_only_runtime_import( checker.package(), checker.settings.isort.detect_same_package, &checker.settings.isort.known_modules, - checker.settings.target_version, + checker.target_version(), checker.settings.isort.no_sections, &checker.settings.isort.section_order, &checker.settings.isort.default_section, diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index feb7437d2fc87..1750f41de82e0 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -152,7 +152,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { ), // PTH115 // Python 3.9+ - ["os", "readlink"] if checker.settings.target_version >= PythonVersion::PY39 => { + ["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => { Some(OsReadlink.into()) } // PTH208, diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index fb1fb328f170d..4ea44ee823924 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -3,7 +3,7 @@ use itertools::{EitherOrBoth, Itertools}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::whitespace::trailing_lines_end; -use ruff_python_ast::{PySourceType, Stmt}; +use ruff_python_ast::{PySourceType, PythonVersion, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::Tokens; @@ -88,6 +88,7 @@ pub(crate) fn organize_imports( package: Option>, source_type: PySourceType, tokens: &Tokens, + target_version: PythonVersion, ) -> Option { let indentation = locator.slice(extract_indentation_range(&block.imports, locator)); let indentation = leading_indentation(indentation); @@ -127,7 +128,7 @@ pub(crate) fn organize_imports( &settings.src, package, source_type, - settings.target_version, + target_version, &settings.isort, tokens, ); diff --git a/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs b/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs index 1ef5dd258fed7..aa53fb5c8ed0d 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs @@ -89,7 +89,7 @@ impl Violation for TryExceptInLoop { /// PERF203 pub(crate) fn try_except_in_loop(checker: &Checker, body: &[Stmt]) { - if checker.settings.target_version >= PythonVersion::PY311 { + if checker.target_version() >= PythonVersion::PY311 { return; } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index 13b3bb333a621..1716b84b7f845 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -229,7 +229,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool { checker.package(), checker.settings.isort.detect_same_package, &checker.settings.isort.known_modules, - checker.settings.target_version, + checker.target_version(), checker.settings.isort.no_sections, &checker.settings.isort.section_order, &checker.settings.isort.default_section, diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs index 9dfe324a75af7..07dd4f8470689 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs @@ -211,7 +211,7 @@ pub(crate) fn bad_str_strip_call(checker: &Checker, call: &ast::ExprCall) { return; } - let removal = if checker.settings.target_version >= PythonVersion::PY39 { + let removal = if checker.target_version() >= PythonVersion::PY39 { RemovalKind::for_strip(strip) } else { None diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs index 573e6b5c04bc2..e8709de61533f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs @@ -76,7 +76,7 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) { } // If this is an allowed dunder method, abort. - if allowed_dunder_constants(attr, checker.settings.target_version) { + if allowed_dunder_constants(attr, checker.target_version()) { return; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs index a5261851caf59..8ed8d8b51511a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs @@ -55,7 +55,7 @@ pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExp return; }; - if is_builtin_exception(func, checker.semantic(), checker.settings.target_version) { + if is_builtin_exception(func, checker.semantic(), checker.target_version()) { let mut diagnostic = Diagnostic::new(UselessExceptionStatement, expr.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( "raise ".to_string(), diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs index 9c83e3cedeaeb..05d00bd6f6e0d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs @@ -722,7 +722,7 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport checker.locator(), checker.stylist(), checker.tokens(), - checker.settings.target_version, + checker.target_version(), ); for (operation, fix) in fixer.without_renames() { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs index 6b031396f64f5..969220eb4481e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -115,7 +115,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) { let Some(version) = extract_version(elts) else { return; }; - let target = checker.settings.target_version; + let target = checker.target_version(); match version_always_less_than( &version, target, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs index ae11deb9eae4f..24f365a2b66d3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs @@ -106,7 +106,7 @@ impl Violation for NonPEP695GenericClass { /// UP046 pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassDef) { // PEP-695 syntax is only available on Python 3.12+ - if checker.settings.target_version < PythonVersion::PY312 { + if checker.target_version() < PythonVersion::PY312 { return; } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs index 197eeae77f3b6..b7d0c99de8dbb 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs @@ -98,7 +98,7 @@ impl Violation for NonPEP695GenericFunction { /// UP047 pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &StmtFunctionDef) { // PEP-695 syntax is only available on Python 3.12+ - if checker.settings.target_version < PythonVersion::PY312 { + if checker.target_version() < PythonVersion::PY312 { return; } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs index d7dd9c8d04229..5895b3e8e9d16 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs @@ -111,7 +111,7 @@ impl Violation for NonPEP695TypeAlias { /// UP040 pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) { - if checker.settings.target_version < PythonVersion::PY312 { + if checker.target_version() < PythonVersion::PY312 { return; } @@ -182,7 +182,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) { /// UP040 pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) { - if checker.settings.target_version < PythonVersion::PY312 { + if checker.target_version() < PythonVersion::PY312 { return; } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs index 159f1a1c3a4b6..b1e030f77ec87 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs @@ -162,7 +162,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except }; match expr.as_ref() { Expr::Name(_) | Expr::Attribute(_) => { - if is_alias(expr, checker.semantic(), checker.settings.target_version) { + if is_alias(expr, checker.semantic(), checker.target_version()) { atom_diagnostic(checker, expr); } } @@ -170,7 +170,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except // List of aliases to replace with `TimeoutError`. let mut aliases: Vec<&Expr> = vec![]; for element in tuple { - if is_alias(element, checker.semantic(), checker.settings.target_version) { + if is_alias(element, checker.semantic(), checker.target_version()) { aliases.push(element); } } @@ -185,7 +185,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except /// UP041 pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) { - if is_alias(func, checker.semantic(), checker.settings.target_version) { + if is_alias(func, checker.semantic(), checker.target_version()) { atom_diagnostic(checker, func); } } @@ -193,7 +193,7 @@ pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) { /// UP041 pub(crate) fn timeout_error_alias_raise(checker: &Checker, expr: &Expr) { if matches!(expr, Expr::Name(_) | Expr::Attribute(_)) { - if is_alias(expr, checker.semantic(), checker.settings.target_version) { + if is_alias(expr, checker.semantic(), checker.target_version()) { atom_diagnostic(checker, expr); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 754bc5a31de48..c9d9af485fdbd 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -98,7 +98,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: checker.semantic(), )?; let binding_edit = Edit::range_replacement(binding, expr.range()); - let applicability = if checker.settings.target_version >= PythonVersion::PY310 { + let applicability = if checker.target_version() >= PythonVersion::PY310 { Applicability::Safe } else { Applicability::Unsafe @@ -122,7 +122,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: Ok(Fix::applicable_edits( import_edit, [reference_edit], - if checker.settings.target_version >= PythonVersion::PY310 { + if checker.target_version() >= PythonVersion::PY310 { Applicability::Safe } else { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 3242f2f24f07d..22d27551dfdee 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -142,7 +142,7 @@ pub(crate) fn non_pep604_annotation( && !checker.semantic().in_complex_string_type_definition() && is_allowed_value(slice); - let applicability = if checker.settings.target_version >= PythonVersion::PY310 { + let applicability = if checker.target_version() >= PythonVersion::PY310 { Applicability::Safe } else { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep646_unpack.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep646_unpack.rs index c2e59836bacd8..50728df17353b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep646_unpack.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep646_unpack.rs @@ -53,7 +53,7 @@ impl Violation for NonPEP646Unpack { /// UP044 pub(crate) fn use_pep646_unpack(checker: &Checker, expr: &ExprSubscript) { - if checker.settings.target_version < PythonVersion::PY311 { + if checker.target_version() < PythonVersion::PY311 { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index ba1f51412a22b..0cd0df8bec87d 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -59,7 +59,7 @@ impl AlwaysFixableViolation for BitCount { /// FURB161 pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { // `int.bit_count()` was added in Python 3.10 - if checker.settings.target_version < PythonVersion::PY310 { + if checker.target_version() < PythonVersion::PY310 { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs index 6779a482a6adc..010aaaa2b62f9 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs @@ -82,7 +82,7 @@ impl AlwaysFixableViolation for FromisoformatReplaceZ { /// FURB162 pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) { - if checker.settings.target_version < PythonVersion::PY311 { + if checker.target_version() < PythonVersion::PY311 { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs index 046640394720d..ee28e60391e80 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs @@ -58,12 +58,7 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) { } // First we go through all the items in the statement and find all `open` operations. - let candidates = find_file_opens( - with, - checker.semantic(), - true, - checker.settings.target_version, - ); + let candidates = find_file_opens(with, checker.semantic(), true, checker.target_version()); if candidates.is_empty() { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs index 4623c008c31c4..1314978dc55b5 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs @@ -69,7 +69,7 @@ impl AlwaysFixableViolation for SliceToRemovePrefixOrSuffix { /// FURB188 pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprIf) { - if checker.settings.target_version < PythonVersion::PY39 { + if checker.target_version() < PythonVersion::PY39 { return; } @@ -100,7 +100,7 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI /// FURB188 pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtIf) { - if checker.settings.target_version < PythonVersion::PY39 { + if checker.target_version() < PythonVersion::PY39 { return; } if let Some(removal_data) = affix_removal_data_stmt(if_stmt) { diff --git a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs index e35320d7cc9db..fa6ac1580ccae 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs @@ -59,12 +59,7 @@ pub(crate) fn write_whole_file(checker: &Checker, with: &ast::StmtWith) { } // First we go through all the items in the statement and find all `open` operations. - let candidates = find_file_opens( - with, - checker.semantic(), - false, - checker.settings.target_version, - ); + let candidates = find_file_opens(with, checker.semantic(), false, checker.target_version()); if candidates.is_empty() { return; } diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs index 846ff0b1091a1..cce3d70a0ae90 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs @@ -79,7 +79,7 @@ impl Violation for ClassWithMixedTypeVars { /// RUF053 pub(crate) fn class_with_mixed_type_vars(checker: &Checker, class_def: &StmtClassDef) { - if checker.settings.target_version < PythonVersion::PY312 { + if checker.target_version() < PythonVersion::PY312 { return; } diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs index de90e3b6e853d..adaec2f99f3e6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs @@ -177,11 +177,11 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) { let Some(expr) = type_hint_explicitly_allows_none( parsed_annotation.expression(), checker, - checker.settings.target_version, + checker.target_version(), ) else { continue; }; - let conversion_type = checker.settings.target_version.into(); + let conversion_type = checker.target_version().into(); let mut diagnostic = Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); @@ -192,14 +192,12 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) { } } else { // Unquoted annotation. - let Some(expr) = type_hint_explicitly_allows_none( - annotation, - checker, - checker.settings.target_version, - ) else { + let Some(expr) = + type_hint_explicitly_allows_none(annotation, checker, checker.target_version()) + else { continue; }; - let conversion_type = checker.settings.target_version.into(); + let conversion_type = checker.target_version().into(); let mut diagnostic = Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); diff --git a/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs index 01f2dd9d0c851..a582cd3c4bbba 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs @@ -88,7 +88,7 @@ pub(crate) fn subscript_with_parenthesized_tuple(checker: &Checker, subscript: & // to a syntax error in Python 3.10. // This is no longer a syntax error starting in Python 3.11 // see https://peps.python.org/pep-0646/#change-1-star-expressions-in-indexes - if checker.settings.target_version <= PythonVersion::PY310 + if checker.target_version() <= PythonVersion::PY310 && !prefer_parentheses && tuple_subscript.iter().any(Expr::is_starred_expr) { diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index d0f2d495e70f5..87d499be609f3 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -8,7 +8,7 @@ use rustc_hash::FxHashSet; use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; -use types::{CompiledPerFileVersion, CompiledPerFileVersionList}; +use types::CompiledPerFileVersionList; use crate::codes::RuleCodePrefix; use ruff_macros::CacheKey; @@ -452,12 +452,9 @@ impl LinterSettings { /// [`LinterSettings::per_file_target_version`] and falls back on /// [`LinterSettings::unresolved_target_version`] if none of the override patterns match. pub fn resolve_target_version(&self, path: &Path) -> PythonVersion { - for CompiledPerFileVersion { matcher, version } in &*self.per_file_target_version { - if matcher.is_match(path) { - return *version; - } - } - self.target_version + self.per_file_target_version + .is_match(path) + .unwrap_or(self.target_version) } } diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 8d757f9b8aa3e..7f0fdf27d8fe3 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -662,6 +662,12 @@ impl CompiledPerFileVersionList { versions: versions?, }) } + + pub fn is_match(&self, path: &Path) -> Option { + self.versions + .iter() + .find_map(|v| v.matcher.is_match(path).then_some(v.version)) + } } impl Deref for CompiledPerFileVersionList { From 89ac994bd46928ebc1e1253c29dc89c9f9d810be Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 13:10:48 -0500 Subject: [PATCH 04/33] allow resolving `per_file_target_version`s in the formatter --- crates/ruff/src/commands/format.rs | 14 ++++++++++++-- crates/ruff_workspace/src/configuration.rs | 8 +++++--- crates/ruff_workspace/src/settings.rs | 20 +++++++++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 5a1c4f97875b8..04994986f302d 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -339,9 +339,17 @@ pub(crate) fn format_source( settings: &FormatterSettings, range: Option, ) -> Result { + let target_version = if let Some(path) = path { + settings.resolve_target_version(path) + } else { + settings.target_version + }; + match &source_kind { SourceKind::Python(unformatted) => { - let options = settings.to_format_options(source_type, unformatted); + let options = settings + .to_format_options(source_type, unformatted) + .with_target_version(target_version); let formatted = if let Some(range) = range { let line_index = LineIndex::from_source_text(unformatted); @@ -391,7 +399,9 @@ pub(crate) fn format_source( )); } - let options = settings.to_format_options(source_type, notebook.source_code()); + let options = settings + .to_format_options(source_type, notebook.source_code()) + .with_target_version(target_version); let mut output: Option = None; let mut last: Option = None; diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 898c72febe270..e3c9b71f11e1e 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -175,11 +175,15 @@ impl Configuration { PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, }; + let per_file_target_version = + CompiledPerFileVersionList::resolve(self.per_file_target_version.unwrap_or_default())?; + 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, + per_file_target_version: per_file_target_version.clone(), line_width: self .line_length .map_or(format_defaults.line_width, |length| { @@ -280,9 +284,7 @@ impl Configuration { extension: self.extension.unwrap_or_default(), preview: lint_preview, target_version, - per_file_target_version: CompiledPerFileVersionList::resolve( - self.per_file_target_version.unwrap_or_default(), - )?, + per_file_target_version, project_root: project_root.to_path_buf(), allowed_confusables: lint .allowed_confusables diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 0ef15d4407249..033986f8ef3d8 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -4,11 +4,12 @@ use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth}; use ruff_graph::AnalyzeSettings; use ruff_linter::display_settings; use ruff_linter::settings::types::{ - ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, UnsafeFixes, + CompiledPerFileVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, + UnsafeFixes, }; use ruff_linter::settings::LinterSettings; use ruff_macros::CacheKey; -use ruff_python_ast::PySourceType; +use ruff_python_ast::{PySourceType, PythonVersion}; use ruff_python_formatter::{ DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle, @@ -164,7 +165,8 @@ pub struct FormatterSettings { pub exclude: FilePatternSet, pub extension: ExtensionMapping, pub preview: PreviewMode, - pub target_version: ruff_python_ast::PythonVersion, + pub target_version: PythonVersion, + pub per_file_target_version: CompiledPerFileVersionList, pub line_width: LineWidth, @@ -216,6 +218,17 @@ impl FormatterSettings { .with_docstring_code(self.docstring_code_format) .with_docstring_code_line_width(self.docstring_code_line_width) } + + /// Resolve the [`PythonVersion`] to use for formatting. + /// + /// This method respects the per-file version overrides in + /// [`FormatterSettings::per_file_target_version`] and falls back on + /// [`FormatterSettings::unresolved_target_version`] if none of the override patterns match. + pub fn resolve_target_version(&self, path: &Path) -> PythonVersion { + self.per_file_target_version + .is_match(path) + .unwrap_or(self.target_version) + } } impl Default for FormatterSettings { @@ -226,6 +239,7 @@ impl Default for FormatterSettings { exclude: FilePatternSet::default(), extension: ExtensionMapping::default(), target_version: default_options.target_version(), + per_file_target_version: CompiledPerFileVersionList::default(), preview: PreviewMode::Disabled, line_width: default_options.line_width(), line_ending: LineEnding::Auto, From 9f7e057ccc5f9f1fabf42a5d568a21e35d16f31c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 13:15:19 -0500 Subject: [PATCH 05/33] resolve target_version once for Checker --- crates/ruff_linter/src/checkers/ast/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 67b3bf40b5393..c4f98433bdd17 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -223,6 +223,8 @@ pub(crate) struct Checker<'a> { last_stmt_end: TextSize, /// A state describing if a docstring is expected or not. docstring_state: DocstringState, + /// The target [`PythonVersion`] for version-dependent checks + target_version: PythonVersion, } impl<'a> Checker<'a> { @@ -273,6 +275,7 @@ impl<'a> Checker<'a> { notebook_index, last_stmt_end: TextSize::default(), docstring_state: DocstringState::default(), + target_version, } } } @@ -502,8 +505,9 @@ impl<'a> Checker<'a> { } } - pub(crate) fn target_version(&self) -> PythonVersion { - self.settings.resolve_target_version(self.path) + /// Return the [`PythonVersion`] to use for version-related checks. + pub(crate) const fn target_version(&self) -> PythonVersion { + self.target_version } } From 90f6493cba82aab87d64a8206704b8b14a28ef16 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 13:49:05 -0500 Subject: [PATCH 06/33] test that the linter respects the per-file version --- crates/ruff/tests/lint.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index ad17c40c74215..07afd83539d28 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -2568,14 +2568,25 @@ fn a005_module_shadowing_strict_default() -> Result<()> { Ok(()) } +/// Test that the linter respects per-file-target-version. #[test] -fn per_file_target_version_exists() { +fn per_file_target_version_linter() { assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .args(STDIN_BASE_OPTIONS) + .args(["--target-version", "py312"]) .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#]) - .args(["--select", "A005"]) // something that won't trigger + .args(["--select", "UP046"]) // only triggers on 3.12+ + .args(["--stdin-filename", "test.py"]) + .arg("--preview") .arg("-") - .pass_stdin("1"), + .pass_stdin(r#" +from typing import Generic, TypeVar + +T = TypeVar("T") + +class A(Generic[T]): + var: T +"#), @r" success: true exit_code: 0 From aceca51899d96bcae84e5bc16619185f46577867 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 13:49:52 -0500 Subject: [PATCH 07/33] handle base and absolute paths like PerFileIgnore --- crates/ruff_linter/src/settings/types.rs | 53 ++++++++++++++++++---- crates/ruff_workspace/src/configuration.rs | 16 +++++-- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 7f0fdf27d8fe3..ab68bd900da98 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -635,9 +635,34 @@ impl Deref for CompiledPerFileIgnoreList { } } +// This struct and its `new` implementation are adapted directly from `PerFileIgnore`, minus the +// `negated` field +#[derive(Debug, Clone)] +pub struct PerFileVersion { + pub(crate) basename: String, + pub(crate) absolute: PathBuf, + pub(crate) version: ast::PythonVersion, +} + +impl PerFileVersion { + pub fn new(pattern: String, version: ast::PythonVersion, project_root: Option<&Path>) -> Self { + let absolute = match project_root { + Some(project_root) => fs::normalize_path_to(&pattern, project_root), + None => fs::normalize_path(&pattern), + }; + + Self { + basename: pattern, + absolute, + version, + } + } +} + #[derive(Debug, Clone, CacheKey)] pub struct CompiledPerFileVersion { - pub matcher: GlobMatcher, + pub absolute_matcher: GlobMatcher, + pub basename_matcher: GlobMatcher, pub version: ast::PythonVersion, } @@ -648,13 +673,21 @@ pub struct CompiledPerFileVersionList { impl CompiledPerFileVersionList { /// Given a list of patterns, create a `GlobSet`. - pub fn resolve(per_file_ignores: FxHashMap) -> Result { + pub fn resolve(per_file_ignores: Vec) -> Result { let versions: Result> = per_file_ignores .into_iter() - .map(|(pattern, version)| { + .map(|per_file_version| { + // Construct absolute path matcher. + let absolute_matcher = + Glob::new(&per_file_version.absolute.to_string_lossy())?.compile_matcher(); + + // Construct basename matcher. + let basename_matcher = Glob::new(&per_file_version.basename)?.compile_matcher(); + Ok(CompiledPerFileVersion { - matcher: Glob::new(&pattern)?.compile_matcher(), - version, + absolute_matcher, + basename_matcher, + version: per_file_version.version, }) }) .collect(); @@ -664,9 +697,13 @@ impl CompiledPerFileVersionList { } pub fn is_match(&self, path: &Path) -> Option { - self.versions - .iter() - .find_map(|v| v.matcher.is_match(path).then_some(v.version)) + self.versions.iter().find_map(|v| { + if v.absolute_matcher.is_match(path) || v.basename_matcher.is_match(path) { + Some(v.version) + } else { + None + } + }) } } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index e3c9b71f11e1e..ffafc3e741201 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -30,7 +30,8 @@ use ruff_linter::settings::fix_safety_table::FixSafetyTable; use ruff_linter::settings::rule_table::RuleTable; use ruff_linter::settings::types::{ CompiledPerFileIgnoreList, CompiledPerFileVersionList, ExtensionMapping, FilePattern, - FilePatternSet, OutputFormat, PerFileIgnore, PreviewMode, RequiredVersion, UnsafeFixes, + FilePatternSet, OutputFormat, PerFileIgnore, PerFileVersion, PreviewMode, RequiredVersion, + UnsafeFixes, }; use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS}; use ruff_linter::{ @@ -175,8 +176,17 @@ impl Configuration { PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, }; - let per_file_target_version = - CompiledPerFileVersionList::resolve(self.per_file_target_version.unwrap_or_default())?; + let per_file_target_version = match self.per_file_target_version { + Some(versions) => CompiledPerFileVersionList::resolve( + versions + .into_iter() + .map(|(pattern, version)| { + PerFileVersion::new(pattern, version, Some(project_root)) + }) + .collect(), + )?, + None => CompiledPerFileVersionList::default(), + }; let formatter = FormatterSettings { exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, From f369358e1811c15159321237c5ce16b28f42cc0e Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 13:54:34 -0500 Subject: [PATCH 08/33] convert to Vec earlier like PerFileIgnore --- crates/ruff_workspace/src/configuration.rs | 35 ++++++++++------------ 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index ffafc3e741201..e6d0235a197c0 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -139,7 +139,7 @@ pub struct Configuration { pub namespace_packages: Option>, pub src: Option>, pub target_version: Option, - pub per_file_target_version: Option>, + pub per_file_target_version: Option>, // Global formatting options pub line_length: Option, @@ -176,17 +176,8 @@ impl Configuration { PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, }; - let per_file_target_version = match self.per_file_target_version { - Some(versions) => CompiledPerFileVersionList::resolve( - versions - .into_iter() - .map(|(pattern, version)| { - PerFileVersion::new(pattern, version, Some(project_root)) - }) - .collect(), - )?, - None => CompiledPerFileVersionList::default(), - }; + let per_file_target_version = + CompiledPerFileVersionList::resolve(self.per_file_target_version.unwrap_or_default())?; let formatter = FormatterSettings { exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, @@ -468,13 +459,6 @@ impl Configuration { } }; - let per_file_target_version = options.per_file_target_version.map(|versions| { - versions - .into_iter() - .map(|(glob, version)| (glob, ast::PythonVersion::from(version))) - .collect() - }); - Ok(Self { builtins: options.builtins, cache_dir: options @@ -556,7 +540,18 @@ impl Configuration { .map(|src| resolve_src(&src, project_root)) .transpose()?, target_version: options.target_version.map(ast::PythonVersion::from), - per_file_target_version, + per_file_target_version: options.per_file_target_version.map(|versions| { + versions + .into_iter() + .map(|(pattern, version)| { + PerFileVersion::new( + pattern, + ast::PythonVersion::from(version), + Some(project_root), + ) + }) + .collect() + }), // `--extension` is a hidden command-line argument that isn't supported in configuration // files at present. extension: None, From ee992b322ee54f5363826cf3eb23f08f5aa252e9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 14:06:27 -0500 Subject: [PATCH 09/33] test that the formatter respects the per-file version --- crates/ruff/tests/format.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/ruff/tests/format.rs b/crates/ruff/tests/format.rs index a24b4e313f5cc..01404eca53d9d 100644 --- a/crates/ruff/tests/format.rs +++ b/crates/ruff/tests/format.rs @@ -2086,3 +2086,31 @@ fn range_formatting_notebook() { error: Failed to format main.ipynb: Range formatting isn't supported for notebooks. "); } + +/// Test that the formatter respects `per-file-target-version`. Context managers can't be +/// parenthesized like this before Python 3.10. +/// +/// Adapted from +#[test] +fn per_file_target_version_formatter() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["format", "--isolated", "--stdin-filename", "test.py", "--target-version=py38"]) + .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#]) + .arg("-") + .pass_stdin(r#" +with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz: + pass +"#), @r#" + success: true + exit_code: 0 + ----- stdout ----- + with ( + open("a_really_long_foo") as foo, + open("a_really_long_bar") as bar, + open("a_really_long_baz") as baz, + ): + pass + + ----- stderr ----- + "#); +} From d9f1c17b70b8622241baf204c5db316d301654cb Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 19 Feb 2025 14:36:08 -0500 Subject: [PATCH 10/33] delete unused Deref impl --- crates/ruff_linter/src/settings/types.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index ab68bd900da98..511c269759b53 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -707,14 +707,6 @@ impl CompiledPerFileVersionList { } } -impl Deref for CompiledPerFileVersionList { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.versions - } -} - #[cfg(test)] mod tests { #[test] From 3bcf6c391ad8afdfb2226fab8eb6c1daa1244852 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 11:32:43 -0500 Subject: [PATCH 11/33] impl Display for CompiledPerFileVersion and use in lint and format --- ...ow_settings__display_default_settings.snap | 2 ++ crates/ruff_linter/src/settings/mod.rs | 1 + crates/ruff_linter/src/settings/types.rs | 31 +++++++++++++++++++ crates/ruff_workspace/src/settings.rs | 1 + 4 files changed, 35 insertions(+) diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index e0bb260b9e2cb..e3656cb65b843 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -190,6 +190,7 @@ linter.per_file_ignores = {} linter.safety_table.forced_safe = [] linter.safety_table.forced_unsafe = [] linter.target_version = 3.7 +linter.per_file_target_version = {} linter.preview = disabled linter.explicit_preview_rules = false linter.extension = ExtensionMapping({}) @@ -374,6 +375,7 @@ linter.ruff.allowed_markup_calls = [] # Formatter Settings formatter.exclude = [] formatter.target_version = 3.7 +formatter.per_file_target_version = {} formatter.preview = disabled formatter.line_width = 100 formatter.line_ending = auto diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 87d499be609f3..202203701aad6 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -284,6 +284,7 @@ impl Display for LinterSettings { self.fix_safety | nested, self.target_version, + self.per_file_target_version, self.preview, self.explicit_preview_rules, self.extension | debug, diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 511c269759b53..b7a634f44d8d9 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -666,6 +666,22 @@ pub struct CompiledPerFileVersion { pub version: ast::PythonVersion, } +impl Display for CompiledPerFileVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + display_settings! { + formatter = f, + fields = [ + self.absolute_matcher | globmatcher, + self.basename_matcher | globmatcher, + // TODO + // self.negated, + self.version, + ] + } + Ok(()) + } +} + #[derive(CacheKey, Clone, Debug, Default)] pub struct CompiledPerFileVersionList { versions: Vec, @@ -707,6 +723,21 @@ impl CompiledPerFileVersionList { } } +impl Display for CompiledPerFileVersionList { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.versions.is_empty() { + write!(f, "{{}}")?; + } else { + writeln!(f, "{{")?; + for version in &self.versions { + writeln!(f, "\t{version}")?; + } + write!(f, "}}")?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { #[test] diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 033986f8ef3d8..f0f7cbfd2edb0 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -262,6 +262,7 @@ impl fmt::Display for FormatterSettings { fields = [ self.exclude, self.target_version, + self.per_file_target_version, self.preview, self.line_width, self.line_ending, From f30a3aa3acf92a40d616a88c52ef31a4163c1979 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 11:36:12 -0500 Subject: [PATCH 12/33] add error case for linter test --- crates/ruff/tests/lint.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index 07afd83539d28..e3348cb22f269 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -2571,6 +2571,36 @@ fn a005_module_shadowing_strict_default() -> Result<()> { /// Test that the linter respects per-file-target-version. #[test] fn per_file_target_version_linter() { + // without per-file-target-version, there should be one UP046 error + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--target-version", "py312"]) + .args(["--select", "UP046"]) // only triggers on 3.12+ + .args(["--stdin-filename", "test.py"]) + .arg("--preview") + .arg("-") + .pass_stdin(r#" +from typing import Generic, TypeVar + +T = TypeVar("T") + +class A(Generic[T]): + var: T +"#), + @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters + Found 1 error. + No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). + + ----- stderr ----- + " + ); + + // with per-file-target-version, there should be no errors because the new generic syntax is + // unavailable assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .args(STDIN_BASE_OPTIONS) .args(["--target-version", "py312"]) From 4c64314c3afb21604809c17e9d9e022059d54ad3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 11:37:52 -0500 Subject: [PATCH 13/33] add other case for formatter test --- crates/ruff/tests/format.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/ruff/tests/format.rs b/crates/ruff/tests/format.rs index 01404eca53d9d..122980d5ed6a2 100644 --- a/crates/ruff/tests/format.rs +++ b/crates/ruff/tests/format.rs @@ -2093,6 +2093,25 @@ fn range_formatting_notebook() { /// Adapted from #[test] fn per_file_target_version_formatter() { + // without `per-file-target-version` this should not be reformatted in the same way + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["format", "--isolated", "--stdin-filename", "test.py", "--target-version=py38"]) + .arg("-") + .pass_stdin(r#" +with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz: + pass +"#), @r#" + success: true + exit_code: 0 + ----- stdout ----- + with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open( + "a_really_long_baz" + ) as baz: + pass + + ----- stderr ----- + "#); + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .args(["format", "--isolated", "--stdin-filename", "test.py", "--target-version=py38"]) .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#]) From 09d2989af7fbdde0ec18f4dccafd47f141cf69ff Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 11:40:48 -0500 Subject: [PATCH 14/33] pass the whole version to is_allowed_module --- .../flake8_builtins/rules/stdlib_module_shadowing.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs index 4a1e154cad337..dc8f4642b5d8d 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs @@ -3,7 +3,7 @@ use std::path::{Component, Path, PathBuf}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::PySourceType; +use ruff_python_ast::{PySourceType, PythonVersion}; use ruff_python_stdlib::path::is_module_file; use ruff_python_stdlib::sys::is_known_standard_library; use ruff_text_size::TextRange; @@ -100,7 +100,7 @@ pub(crate) fn stdlib_module_shadowing( if is_allowed_module( settings, - settings.resolve_target_version(&path).minor, + settings.resolve_target_version(&path), &module_name, ) { return None; @@ -133,7 +133,7 @@ fn get_prefix<'a>(settings: &'a LinterSettings, path: &Path) -> Option<&'a PathB prefix } -fn is_allowed_module(settings: &LinterSettings, minor_version: u8, module: &str) -> bool { +fn is_allowed_module(settings: &LinterSettings, version: PythonVersion, module: &str) -> bool { // Shadowing private stdlib modules is okay. // https://github.com/astral-sh/ruff/issues/12949 if module.starts_with('_') && !module.starts_with("__") { @@ -149,5 +149,5 @@ fn is_allowed_module(settings: &LinterSettings, minor_version: u8, module: &str) return true; } - !is_known_standard_library(minor_version, module) + !is_known_standard_library(version.minor, module) } From c1bd3a02a191ceb0b96aaaba005980b4d0f58388 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 11:43:23 -0500 Subject: [PATCH 15/33] pass target_version down to A005 --- crates/ruff_linter/src/checkers/filesystem.rs | 4 +++- crates/ruff_linter/src/linter.rs | 4 +++- .../rules/flake8_builtins/rules/stdlib_module_shadowing.rs | 7 ++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/checkers/filesystem.rs b/crates/ruff_linter/src/checkers/filesystem.rs index 98ee3b80c3912..f6d9c491c8ccc 100644 --- a/crates/ruff_linter/src/checkers/filesystem.rs +++ b/crates/ruff_linter/src/checkers/filesystem.rs @@ -1,6 +1,7 @@ use std::path::Path; use ruff_diagnostics::Diagnostic; +use ruff_python_ast::PythonVersion; use ruff_python_trivia::CommentRanges; use crate::package::PackageRoot; @@ -17,6 +18,7 @@ pub(crate) fn check_file_path( locator: &Locator, comment_ranges: &CommentRanges, settings: &LinterSettings, + target_version: PythonVersion, ) -> Vec { let mut diagnostics: Vec = vec![]; @@ -46,7 +48,7 @@ pub(crate) fn check_file_path( // flake8-builtins if settings.rules.enabled(Rule::StdlibModuleShadowing) { - if let Some(diagnostic) = stdlib_module_shadowing(path, settings) { + if let Some(diagnostic) = stdlib_module_shadowing(path, settings, target_version) { diagnostics.push(diagnostic); } } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 6c05ebdcd62b4..4179c3b3815fc 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -104,6 +104,8 @@ pub fn check_path( )); } + let target_version = settings.resolve_target_version(path); + // Run the filesystem-based rules. if settings .rules @@ -116,6 +118,7 @@ pub fn check_path( locator, comment_ranges, settings, + target_version, )); } @@ -144,7 +147,6 @@ pub fn check_path( if use_ast || use_imports || use_doc_lines { let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets); let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index); - let target_version = settings.resolve_target_version(path); if use_ast { diagnostics.extend(check_ast( parsed, diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs index dc8f4642b5d8d..95f5c00611afc 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs @@ -69,6 +69,7 @@ impl Violation for StdlibModuleShadowing { pub(crate) fn stdlib_module_shadowing( mut path: &Path, settings: &LinterSettings, + target_version: PythonVersion, ) -> Option { if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file) { return None; @@ -98,11 +99,7 @@ pub(crate) fn stdlib_module_shadowing( let module_name = components.next()?; - if is_allowed_module( - settings, - settings.resolve_target_version(&path), - &module_name, - ) { + if is_allowed_module(settings, target_version, &module_name) { return None; } From 89bee8d8ce25df19b9937418b14f940f4497757b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 11:57:40 -0500 Subject: [PATCH 16/33] extract PerFile and share negation with PerFileVersion --- crates/ruff_linter/src/settings/types.rs | 70 +++++++++++------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index b7a634f44d8d9..6eed008dc05e5 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -275,21 +275,15 @@ impl CacheKey for FilePatternSet { } #[derive(Debug, Clone)] -pub struct PerFileIgnore { - pub(crate) basename: String, - pub(crate) absolute: PathBuf, - pub(crate) negated: bool, - pub(crate) rules: RuleSet, +struct PerFile { + basename: String, + absolute: PathBuf, + negated: bool, + data: T, } -impl PerFileIgnore { - pub fn new( - mut pattern: String, - prefixes: &[RuleSelector], - project_root: Option<&Path>, - ) -> Self { - // Rules in preview are included here even if preview mode is disabled; it's safe to ignore disabled rules - let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect(); +impl PerFile { + fn new(mut pattern: String, project_root: Option<&Path>, data: T) -> Self { let negated = pattern.starts_with('!'); if negated { pattern.drain(..1); @@ -304,11 +298,23 @@ impl PerFileIgnore { basename: pattern, absolute, negated, - rules, + data, } } } +#[derive(Debug, Clone)] +pub struct PerFileIgnore(PerFile); + +impl PerFileIgnore { + pub fn new(pattern: String, prefixes: &[RuleSelector], project_root: Option<&Path>) -> Self { + // Rules in preview are included here even if preview mode is disabled; it's safe to ignore + // disabled rules + let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect(); + Self(PerFile::new(pattern, project_root, rules)) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct PatternPrefixPair { pub pattern: String, @@ -595,16 +601,16 @@ impl CompiledPerFileIgnoreList { .map(|per_file_ignore| { // Construct absolute path matcher. let absolute_matcher = - Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher(); + Glob::new(&per_file_ignore.0.absolute.to_string_lossy())?.compile_matcher(); // Construct basename matcher. - let basename_matcher = Glob::new(&per_file_ignore.basename)?.compile_matcher(); + let basename_matcher = Glob::new(&per_file_ignore.0.basename)?.compile_matcher(); Ok(CompiledPerFileIgnore { absolute_matcher, basename_matcher, - negated: per_file_ignore.negated, - rules: per_file_ignore.rules, + negated: per_file_ignore.0.negated, + rules: per_file_ignore.0.data, }) }) .collect(); @@ -638,24 +644,11 @@ impl Deref for CompiledPerFileIgnoreList { // This struct and its `new` implementation are adapted directly from `PerFileIgnore`, minus the // `negated` field #[derive(Debug, Clone)] -pub struct PerFileVersion { - pub(crate) basename: String, - pub(crate) absolute: PathBuf, - pub(crate) version: ast::PythonVersion, -} +pub struct PerFileVersion(PerFile); impl PerFileVersion { pub fn new(pattern: String, version: ast::PythonVersion, project_root: Option<&Path>) -> Self { - let absolute = match project_root { - Some(project_root) => fs::normalize_path_to(&pattern, project_root), - None => fs::normalize_path(&pattern), - }; - - Self { - basename: pattern, - absolute, - version, - } + Self(PerFile::new(pattern, project_root, version)) } } @@ -663,6 +656,7 @@ impl PerFileVersion { pub struct CompiledPerFileVersion { pub absolute_matcher: GlobMatcher, pub basename_matcher: GlobMatcher, + pub negated: bool, pub version: ast::PythonVersion, } @@ -673,8 +667,7 @@ impl Display for CompiledPerFileVersion { fields = [ self.absolute_matcher | globmatcher, self.basename_matcher | globmatcher, - // TODO - // self.negated, + self.negated, self.version, ] } @@ -695,15 +688,16 @@ impl CompiledPerFileVersionList { .map(|per_file_version| { // Construct absolute path matcher. let absolute_matcher = - Glob::new(&per_file_version.absolute.to_string_lossy())?.compile_matcher(); + Glob::new(&per_file_version.0.absolute.to_string_lossy())?.compile_matcher(); // Construct basename matcher. - let basename_matcher = Glob::new(&per_file_version.basename)?.compile_matcher(); + let basename_matcher = Glob::new(&per_file_version.0.basename)?.compile_matcher(); Ok(CompiledPerFileVersion { absolute_matcher, basename_matcher, - version: per_file_version.version, + negated: per_file_version.0.negated, + version: per_file_version.0.data, }) }) .collect(); From b364a4ad5047464231c938ea364b9372ee5416a1 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 12:19:39 -0500 Subject: [PATCH 17/33] add some docs --- crates/ruff_linter/src/settings/types.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 6eed008dc05e5..d402913d0b3c9 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -274,15 +274,24 @@ impl CacheKey for FilePatternSet { } } +/// A glob pattern and associated data for matching file paths. #[derive(Debug, Clone)] -struct PerFile { +pub struct PerFile { + /// The glob pattern used to construct the [`PerFile`]. basename: String, + /// The same pattern as `basename` but normalized to the project root directory. absolute: PathBuf, + /// Whether the glob pattern should be negated (e.g. `!*.ipynb`) negated: bool, + /// The per-file data associated with these glob patterns. data: T, } impl PerFile { + /// Construct a new [`PerFile`] from the given glob `pattern` and containing `data`. + /// + /// If provided, `project_root` is used to construct a second glob pattern normalized to the + /// project root directory. See [`fs::normalize_path_to`] for more details. fn new(mut pattern: String, project_root: Option<&Path>, data: T) -> Self { let negated = pattern.starts_with('!'); if negated { @@ -303,6 +312,7 @@ impl PerFile { } } +/// Per-file ignored linting rules. #[derive(Debug, Clone)] pub struct PerFileIgnore(PerFile); @@ -564,6 +574,7 @@ impl Display for RequiredVersion { /// pattern matching. pub type IdentifierPattern = glob::Pattern; +/// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage. #[derive(Debug, Clone, CacheKey)] pub struct CompiledPerFileIgnore { pub absolute_matcher: GlobMatcher, @@ -641,8 +652,9 @@ impl Deref for CompiledPerFileIgnoreList { } } -// This struct and its `new` implementation are adapted directly from `PerFileIgnore`, minus the -// `negated` field +/// Contains the target Python version for a given glob pattern. +/// +/// See [`PerFile`] for details of the representation. #[derive(Debug, Clone)] pub struct PerFileVersion(PerFile); From 6cf6ccbc63ec6f1ff54583ba6844964b3acf977d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 12:20:19 -0500 Subject: [PATCH 18/33] factor out CompiledPerFile --- crates/ruff_linter/src/fs.rs | 12 ++-- crates/ruff_linter/src/settings/types.rs | 84 +++++++++++++----------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/crates/ruff_linter/src/fs.rs b/crates/ruff_linter/src/fs.rs index 39a09b2ed115e..cdc2525c3a432 100644 --- a/crates/ruff_linter/src/fs.rs +++ b/crates/ruff_linter/src/fs.rs @@ -18,9 +18,9 @@ pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnore "Adding per-file ignores for {:?} due to basename match on {:?}: {:?}", path, entry.basename_matcher.glob().regex(), - entry.rules + entry.data ); - Some(&entry.rules) + Some(&entry.data) } } else if entry.absolute_matcher.is_match(path) { if entry.negated { None } else { @@ -28,9 +28,9 @@ pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnore "Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}", path, entry.absolute_matcher.glob().regex(), - entry.rules + entry.data ); - Some(&entry.rules) + Some(&entry.data) } } else if entry.negated { debug!( @@ -38,9 +38,9 @@ pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnore path, entry.basename_matcher.glob().regex(), entry.absolute_matcher.glob().regex(), - entry.rules + entry.data ); - Some(&entry.rules) + Some(&entry.data) } else { None } diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index d402913d0b3c9..711238fbe1cfc 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -576,14 +576,33 @@ pub type IdentifierPattern = glob::Pattern; /// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage. #[derive(Debug, Clone, CacheKey)] -pub struct CompiledPerFileIgnore { +pub struct CompiledPerFile { pub absolute_matcher: GlobMatcher, pub basename_matcher: GlobMatcher, pub negated: bool, - pub rules: RuleSet, + pub data: T, } -impl Display for CompiledPerFileIgnore { +impl CompiledPerFile { + fn new( + absolute_matcher: GlobMatcher, + basename_matcher: GlobMatcher, + negated: bool, + data: T, + ) -> Self { + Self { + absolute_matcher, + basename_matcher, + negated, + data, + } + } +} + +impl Display for CompiledPerFile +where + T: Display + CacheKey, +{ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { display_settings! { formatter = f, @@ -591,13 +610,24 @@ impl Display for CompiledPerFileIgnore { self.absolute_matcher | globmatcher, self.basename_matcher | globmatcher, self.negated, - self.rules, + self.data, ] } Ok(()) } } +#[derive(Debug, Clone, CacheKey)] +pub struct CompiledPerFileIgnore(CompiledPerFile); + +impl Deref for CompiledPerFileIgnore { + type Target = CompiledPerFile; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Debug, Clone, CacheKey, Default)] pub struct CompiledPerFileIgnoreList { // Ordered as (absolute path matcher, basename matcher, rules) @@ -617,12 +647,12 @@ impl CompiledPerFileIgnoreList { // Construct basename matcher. let basename_matcher = Glob::new(&per_file_ignore.0.basename)?.compile_matcher(); - Ok(CompiledPerFileIgnore { + Ok(CompiledPerFileIgnore(CompiledPerFile::new( absolute_matcher, basename_matcher, - negated: per_file_ignore.0.negated, - rules: per_file_ignore.0.data, - }) + per_file_ignore.0.negated, + per_file_ignore.0.data, + ))) }) .collect(); Ok(Self { ignores: ignores? }) @@ -636,7 +666,7 @@ impl Display for CompiledPerFileIgnoreList { } else { writeln!(f, "{{")?; for ignore in &self.ignores { - writeln!(f, "\t{ignore}")?; + writeln!(f, "\t{}", ignore.0)?; } write!(f, "}}")?; } @@ -665,27 +695,7 @@ impl PerFileVersion { } #[derive(Debug, Clone, CacheKey)] -pub struct CompiledPerFileVersion { - pub absolute_matcher: GlobMatcher, - pub basename_matcher: GlobMatcher, - pub negated: bool, - pub version: ast::PythonVersion, -} - -impl Display for CompiledPerFileVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - display_settings! { - formatter = f, - fields = [ - self.absolute_matcher | globmatcher, - self.basename_matcher | globmatcher, - self.negated, - self.version, - ] - } - Ok(()) - } -} +pub struct CompiledPerFileVersion(CompiledPerFile); #[derive(CacheKey, Clone, Debug, Default)] pub struct CompiledPerFileVersionList { @@ -705,12 +715,12 @@ impl CompiledPerFileVersionList { // Construct basename matcher. let basename_matcher = Glob::new(&per_file_version.0.basename)?.compile_matcher(); - Ok(CompiledPerFileVersion { + Ok(CompiledPerFileVersion(CompiledPerFile::new( absolute_matcher, basename_matcher, - negated: per_file_version.0.negated, - version: per_file_version.0.data, - }) + per_file_version.0.negated, + per_file_version.0.data, + ))) }) .collect(); Ok(Self { @@ -720,8 +730,8 @@ impl CompiledPerFileVersionList { pub fn is_match(&self, path: &Path) -> Option { self.versions.iter().find_map(|v| { - if v.absolute_matcher.is_match(path) || v.basename_matcher.is_match(path) { - Some(v.version) + if v.0.absolute_matcher.is_match(path) || v.0.basename_matcher.is_match(path) { + Some(v.0.data) } else { None } @@ -736,7 +746,7 @@ impl Display for CompiledPerFileVersionList { } else { writeln!(f, "{{")?; for version in &self.versions { - writeln!(f, "\t{version}")?; + writeln!(f, "\t{}", version.0)?; } write!(f, "}}")?; } From 1c85afa9262dc31ad97e16298d61b41424a22cf4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 13:11:34 -0500 Subject: [PATCH 19/33] add generic List type and factor out uses --- crates/ruff_linter/src/fs.rs | 41 +----- crates/ruff_linter/src/settings/types.rs | 158 ++++++++++++++--------- 2 files changed, 98 insertions(+), 101 deletions(-) diff --git a/crates/ruff_linter/src/fs.rs b/crates/ruff_linter/src/fs.rs index cdc2525c3a432..1528d5aec1ac8 100644 --- a/crates/ruff_linter/src/fs.rs +++ b/crates/ruff_linter/src/fs.rs @@ -1,6 +1,5 @@ use std::path::{Path, PathBuf}; -use log::debug; use path_absolutize::Absolutize; use crate::registry::RuleSet; @@ -8,45 +7,7 @@ use crate::settings::types::CompiledPerFileIgnoreList; /// Create a set with codes matching the pattern/code pairs. pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnoreList) -> RuleSet { - let file_name = path.file_name().expect("Unable to parse filename"); - ignore_list - .iter() - .filter_map(|entry| { - if entry.basename_matcher.is_match(file_name) { - if entry.negated { None } else { - debug!( - "Adding per-file ignores for {:?} due to basename match on {:?}: {:?}", - path, - entry.basename_matcher.glob().regex(), - entry.data - ); - Some(&entry.data) - } - } else if entry.absolute_matcher.is_match(path) { - if entry.negated { None } else { - debug!( - "Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}", - path, - entry.absolute_matcher.glob().regex(), - entry.data - ); - Some(&entry.data) - } - } else if entry.negated { - debug!( - "Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}", - path, - entry.basename_matcher.glob().regex(), - entry.absolute_matcher.glob().regex(), - entry.data - ); - Some(&entry.data) - } else { - None - } - }) - .flatten() - .collect() + ignore_list.iter_matches(path).flatten().collect() } /// Convert any path to an absolute path (based on the current working diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 711238fbe1cfc..d513d1ee00047 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -7,6 +7,7 @@ use std::string::ToString; use anyhow::{bail, Result}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; +use log::debug; use pep440_rs::{VersionSpecifier, VersionSpecifiers}; use rustc_hash::FxHashMap; use serde::{de, Deserialize, Deserializer, Serialize}; @@ -629,44 +630,94 @@ impl Deref for CompiledPerFileIgnore { } #[derive(Debug, Clone, CacheKey, Default)] -pub struct CompiledPerFileIgnoreList { - // Ordered as (absolute path matcher, basename matcher, rules) - ignores: Vec, +pub struct CompiledPerFileList { + inner: Vec>, } -impl CompiledPerFileIgnoreList { - /// Given a list of patterns, create a `GlobSet`. - pub fn resolve(per_file_ignores: Vec) -> Result { - let ignores: Result> = per_file_ignores +impl CompiledPerFileList { + /// Given a list of [`PerFile`] patterns, create a compiled set of globs. + fn resolve(per_file_items: impl IntoIterator>) -> Result { + let inner: Result> = per_file_items .into_iter() .map(|per_file_ignore| { // Construct absolute path matcher. let absolute_matcher = - Glob::new(&per_file_ignore.0.absolute.to_string_lossy())?.compile_matcher(); + Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher(); // Construct basename matcher. - let basename_matcher = Glob::new(&per_file_ignore.0.basename)?.compile_matcher(); + let basename_matcher = Glob::new(&per_file_ignore.basename)?.compile_matcher(); - Ok(CompiledPerFileIgnore(CompiledPerFile::new( + Ok(CompiledPerFile::new( absolute_matcher, basename_matcher, - per_file_ignore.0.negated, - per_file_ignore.0.data, - ))) + per_file_ignore.negated, + per_file_ignore.data, + )) }) .collect(); - Ok(Self { ignores: ignores? }) + Ok(Self { inner: inner? }) + } + + pub(crate) fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub(crate) fn iter_matches<'a, 'p>(&'a self, path: &'p Path) -> impl Iterator + where + 'a: 'p, + { + let file_name = path.file_name().expect("Unable to parse filename"); + self.inner + .iter() + .filter_map(move |entry| { + if entry.basename_matcher.is_match(file_name) { + if entry.negated { None } else { + // TODO need to pass a name here + debug!( + "Adding per-file ignores for {:?} due to basename match on {:?}: {:?}", + path, + entry.basename_matcher.glob().regex(), + entry.data + ); + Some(&entry.data) + } + } else if entry.absolute_matcher.is_match(path) { + if entry.negated { None } else { + debug!( + "Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}", + path, + entry.absolute_matcher.glob().regex(), + entry.data + ); + Some(&entry.data) + } + } else if entry.negated { + debug!( + "Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}", + path, + entry.basename_matcher.glob().regex(), + entry.absolute_matcher.glob().regex(), + entry.data + ); + Some(&entry.data) + } else { + None + } + }) } } -impl Display for CompiledPerFileIgnoreList { +impl Display for CompiledPerFileList +where + T: Display + CacheKey, +{ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.ignores.is_empty() { + if self.inner.is_empty() { write!(f, "{{}}")?; } else { writeln!(f, "{{")?; - for ignore in &self.ignores { - writeln!(f, "\t{}", ignore.0)?; + for value in &self.inner { + writeln!(f, "\t{value}")?; } write!(f, "}}")?; } @@ -674,11 +725,31 @@ impl Display for CompiledPerFileIgnoreList { } } +#[derive(Debug, Clone, CacheKey, Default)] +pub struct CompiledPerFileIgnoreList(CompiledPerFileList); + +impl CompiledPerFileIgnoreList { + /// Given a list of [`PerFileIgnore`] patterns, create a compiled set of globs. + /// + /// Returns an error if either of the glob patterns cannot be parsed. + pub fn resolve(per_file_ignores: Vec) -> Result { + Ok(Self(CompiledPerFileList::resolve( + per_file_ignores.into_iter().map(|ignore| ignore.0), + )?)) + } +} + impl Deref for CompiledPerFileIgnoreList { - type Target = Vec; + type Target = CompiledPerFileList; fn deref(&self) -> &Self::Target { - &self.ignores + &self.0 + } +} + +impl Display for CompiledPerFileIgnoreList { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) } } @@ -698,59 +769,24 @@ impl PerFileVersion { pub struct CompiledPerFileVersion(CompiledPerFile); #[derive(CacheKey, Clone, Debug, Default)] -pub struct CompiledPerFileVersionList { - versions: Vec, -} +pub struct CompiledPerFileVersionList(CompiledPerFileList); impl CompiledPerFileVersionList { /// Given a list of patterns, create a `GlobSet`. pub fn resolve(per_file_ignores: Vec) -> Result { - let versions: Result> = per_file_ignores - .into_iter() - .map(|per_file_version| { - // Construct absolute path matcher. - let absolute_matcher = - Glob::new(&per_file_version.0.absolute.to_string_lossy())?.compile_matcher(); - - // Construct basename matcher. - let basename_matcher = Glob::new(&per_file_version.0.basename)?.compile_matcher(); - - Ok(CompiledPerFileVersion(CompiledPerFile::new( - absolute_matcher, - basename_matcher, - per_file_version.0.negated, - per_file_version.0.data, - ))) - }) - .collect(); - Ok(Self { - versions: versions?, - }) + Ok(Self(CompiledPerFileList::resolve( + per_file_ignores.into_iter().map(|version| version.0), + )?)) } pub fn is_match(&self, path: &Path) -> Option { - self.versions.iter().find_map(|v| { - if v.0.absolute_matcher.is_match(path) || v.0.basename_matcher.is_match(path) { - Some(v.0.data) - } else { - None - } - }) + self.0.iter_matches(path).next().copied() } } impl Display for CompiledPerFileVersionList { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.versions.is_empty() { - write!(f, "{{}}")?; - } else { - writeln!(f, "{{")?; - for version in &self.versions { - writeln!(f, "\t{}", version.0)?; - } - write!(f, "}}")?; - } - Ok(()) + self.0.fmt(f) } } From 77653e6586f52264f284d27c72f99b9fc0512f52 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 13:21:54 -0500 Subject: [PATCH 20/33] remove unused CompiledPerFile newtypes --- crates/ruff_linter/src/settings/types.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index d513d1ee00047..15a4ce0bbd9c3 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -618,17 +618,7 @@ where } } -#[derive(Debug, Clone, CacheKey)] -pub struct CompiledPerFileIgnore(CompiledPerFile); - -impl Deref for CompiledPerFileIgnore { - type Target = CompiledPerFile; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - +/// A sequence of [`CompiledPerFile`]. #[derive(Debug, Clone, CacheKey, Default)] pub struct CompiledPerFileList { inner: Vec>, @@ -764,10 +754,6 @@ impl PerFileVersion { Self(PerFile::new(pattern, project_root, version)) } } - -#[derive(Debug, Clone, CacheKey)] -pub struct CompiledPerFileVersion(CompiledPerFile); - #[derive(CacheKey, Clone, Debug, Default)] pub struct CompiledPerFileVersionList(CompiledPerFileList); From 920df2dcf4775a24cb342f2d642d9cbb32458873 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 13:39:58 -0500 Subject: [PATCH 21/33] add PerFileKind to fix labels --- crates/ruff_linter/src/settings/types.rs | 40 ++++++++++++++++++------ 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 15a4ce0bbd9c3..6e44a39f3fe2d 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -624,7 +624,20 @@ pub struct CompiledPerFileList { inner: Vec>, } -impl CompiledPerFileList { +/// Helper trait for debug labels on [`PerFile`] types. +pub trait PerFileKind { + const LABEL: &str; +} + +impl PerFileKind for RuleSet { + const LABEL: &str = "Adding per-file ignores"; +} + +impl PerFileKind for ast::PythonVersion { + const LABEL: &str = "Setting Python version"; +} + +impl CompiledPerFileList { /// Given a list of [`PerFile`] patterns, create a compiled set of globs. fn resolve(per_file_items: impl IntoIterator>) -> Result { let inner: Result> = per_file_items @@ -651,20 +664,22 @@ impl CompiledPerFileList { pub(crate) fn is_empty(&self) -> bool { self.inner.is_empty() } +} +impl CompiledPerFileList { pub(crate) fn iter_matches<'a, 'p>(&'a self, path: &'p Path) -> impl Iterator where 'a: 'p, { let file_name = path.file_name().expect("Unable to parse filename"); - self.inner - .iter() - .filter_map(move |entry| { + self.inner.iter().filter_map(move |entry| { if entry.basename_matcher.is_match(file_name) { - if entry.negated { None } else { - // TODO need to pass a name here + if entry.negated { + None + } else { debug!( - "Adding per-file ignores for {:?} due to basename match on {:?}: {:?}", + "{} for {:?} due to basename match on {:?}: {:?}", + T::LABEL, path, entry.basename_matcher.glob().regex(), entry.data @@ -672,9 +687,12 @@ impl CompiledPerFileList { Some(&entry.data) } } else if entry.absolute_matcher.is_match(path) { - if entry.negated { None } else { + if entry.negated { + None + } else { debug!( - "Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}", + "{} for {:?} due to absolute match on {:?}: {:?}", + T::LABEL, path, entry.absolute_matcher.glob().regex(), entry.data @@ -683,7 +701,8 @@ impl CompiledPerFileList { } } else if entry.negated { debug!( - "Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}", + "{} for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}", + T::LABEL, path, entry.basename_matcher.glob().regex(), entry.absolute_matcher.glob().regex(), @@ -754,6 +773,7 @@ impl PerFileVersion { Self(PerFile::new(pattern, project_root, version)) } } + #[derive(CacheKey, Clone, Debug, Default)] pub struct CompiledPerFileVersionList(CompiledPerFileList); From 5d02c6096fbb1763c010f7d32bb4b4e1172c4149 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 13:41:52 -0500 Subject: [PATCH 22/33] rename to PerFileTargetVersion --- crates/ruff_linter/src/settings/types.rs | 10 ++++++---- crates/ruff_workspace/src/configuration.rs | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 6e44a39f3fe2d..085bf1e404c5f 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -766,9 +766,9 @@ impl Display for CompiledPerFileIgnoreList { /// /// See [`PerFile`] for details of the representation. #[derive(Debug, Clone)] -pub struct PerFileVersion(PerFile); +pub struct PerFileTargetVersion(PerFile); -impl PerFileVersion { +impl PerFileTargetVersion { pub fn new(pattern: String, version: ast::PythonVersion, project_root: Option<&Path>) -> Self { Self(PerFile::new(pattern, project_root, version)) } @@ -778,8 +778,10 @@ impl PerFileVersion { pub struct CompiledPerFileVersionList(CompiledPerFileList); impl CompiledPerFileVersionList { - /// Given a list of patterns, create a `GlobSet`. - pub fn resolve(per_file_ignores: Vec) -> Result { + /// Given a list of [`PerFileTargetVersion`] patterns, create a compiled set of globs. + /// + /// Returns an error if either of the glob patterns cannot be parsed. + pub fn resolve(per_file_ignores: Vec) -> Result { Ok(Self(CompiledPerFileList::resolve( per_file_ignores.into_iter().map(|version| version.0), )?)) diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index e6d0235a197c0..ea87c3d1f5de6 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -30,8 +30,8 @@ use ruff_linter::settings::fix_safety_table::FixSafetyTable; use ruff_linter::settings::rule_table::RuleTable; use ruff_linter::settings::types::{ CompiledPerFileIgnoreList, CompiledPerFileVersionList, ExtensionMapping, FilePattern, - FilePatternSet, OutputFormat, PerFileIgnore, PerFileVersion, PreviewMode, RequiredVersion, - UnsafeFixes, + FilePatternSet, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode, + RequiredVersion, UnsafeFixes, }; use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS}; use ruff_linter::{ @@ -139,7 +139,7 @@ pub struct Configuration { pub namespace_packages: Option>, pub src: Option>, pub target_version: Option, - pub per_file_target_version: Option>, + pub per_file_target_version: Option>, // Global formatting options pub line_length: Option, @@ -544,7 +544,7 @@ impl Configuration { versions .into_iter() .map(|(pattern, version)| { - PerFileVersion::new( + PerFileTargetVersion::new( pattern, ast::PythonVersion::from(version), Some(project_root), From 276f3ab52b3913e632aabfab13c5c91ac4e8d019 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 13:53:33 -0500 Subject: [PATCH 23/33] add context on glob failure --- crates/ruff_linter/src/settings/types.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 085bf1e404c5f..efb45ab7f71ad 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::string::ToString; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use log::debug; use pep440_rs::{VersionSpecifier, VersionSpecifiers}; @@ -639,16 +639,21 @@ impl PerFileKind for ast::PythonVersion { impl CompiledPerFileList { /// Given a list of [`PerFile`] patterns, create a compiled set of globs. + /// + /// Returns an error if either of the glob patterns cannot be parsed. fn resolve(per_file_items: impl IntoIterator>) -> Result { let inner: Result> = per_file_items .into_iter() .map(|per_file_ignore| { // Construct absolute path matcher. - let absolute_matcher = - Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher(); + let absolute_matcher = Glob::new(&per_file_ignore.absolute.to_string_lossy()) + .with_context(|| format!("invalid glob {:?}", per_file_ignore.absolute))? + .compile_matcher(); // Construct basename matcher. - let basename_matcher = Glob::new(&per_file_ignore.basename)?.compile_matcher(); + let basename_matcher = Glob::new(&per_file_ignore.basename) + .with_context(|| format!("invalid glob {:?}", per_file_ignore.basename))? + .compile_matcher(); Ok(CompiledPerFile::new( absolute_matcher, From 699f273752ac3af80abf76b5d8bd11887be212bf Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 13:55:52 -0500 Subject: [PATCH 24/33] also add context to resolve call itself --- crates/ruff_workspace/src/configuration.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index ea87c3d1f5de6..799781faf66f8 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -9,7 +9,7 @@ use std::num::{NonZeroU16, NonZeroU8}; use std::path::{Path, PathBuf}; use std::str::FromStr; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use glob::{glob, GlobError, Paths, PatternError}; use itertools::Itertools; use regex::Regex; @@ -177,7 +177,8 @@ impl Configuration { }; let per_file_target_version = - CompiledPerFileVersionList::resolve(self.per_file_target_version.unwrap_or_default())?; + CompiledPerFileVersionList::resolve(self.per_file_target_version.unwrap_or_default()) + .context("failed to resolve `per-file-target-version` table")?; let formatter = FormatterSettings { exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, From 5f7e8b015398a1b04be9aca7a5955f017ed47d1b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 14:04:01 -0500 Subject: [PATCH 25/33] expand docs on `per-file-target-version` --- crates/ruff_workspace/src/options.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 7b258c9ccf9a4..01d9ada89cf3c 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -333,8 +333,18 @@ pub struct Options { )] pub target_version: Option, - /// A list of mappings from file pattern to Python version to use when checking the + /// A list of mappings from glob-style file pattern to Python version to use when checking the /// corresponding file(s). + /// + /// This may be useful for overriding the global Python version settings in `target-version` or + /// `requires-python` for a subset of files. For example, if you have a project with a minimum + /// supported Python version of 3.9 but a subdirectory of developer scripts that want to use a + /// newer feature like the `match` statement from Python 3.10, you can use + /// `per-file-target-version` to specify `"developer_scripts/*.py" = "py310"`. + /// + /// This setting is used by the linter to enforce any enabled version-specific lint rules, as + /// well as by the formatter for any version-specific formatting options, such as parenthesizing + /// context managers on Python 3.10+. #[option( default = "{}", value_type = "dict[str, PythonVersion]", From 1f4dced545f6a0fb936078ff77daa7d3f3f94f48 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 14:12:21 -0500 Subject: [PATCH 26/33] rename target_version fields and document them along with per-file --- crates/ruff/src/commands/format.rs | 2 +- crates/ruff_linter/src/rules/fastapi/mod.rs | 2 +- .../src/rules/flake8_annotations/mod.rs | 2 +- .../ruff_linter/src/rules/flake8_async/mod.rs | 2 +- .../src/rules/flake8_bugbear/mod.rs | 2 +- .../src/rules/flake8_builtins/mod.rs | 2 +- .../rules/flake8_future_annotations/mod.rs | 4 ++-- .../ruff_linter/src/rules/flake8_pyi/mod.rs | 2 +- .../src/rules/flake8_type_checking/mod.rs | 2 +- crates/ruff_linter/src/rules/perflint/mod.rs | 2 +- crates/ruff_linter/src/rules/pyflakes/mod.rs | 2 +- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 20 ++++++++-------- crates/ruff_linter/src/rules/ruff/mod.rs | 2 +- crates/ruff_linter/src/settings/mod.rs | 23 +++++++++++++------ crates/ruff_workspace/src/configuration.rs | 4 ++-- crates/ruff_workspace/src/settings.rs | 19 +++++++++++---- 16 files changed, 55 insertions(+), 37 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 04994986f302d..af55cedc8c79c 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -342,7 +342,7 @@ pub(crate) fn format_source( let target_version = if let Some(path) = path { settings.resolve_target_version(path) } else { - settings.target_version + settings.unresolved_target_version }; match &source_kind { diff --git a/crates/ruff_linter/src/rules/fastapi/mod.rs b/crates/ruff_linter/src/rules/fastapi/mod.rs index 91e77e6a5c2aa..70d22c502016b 100644 --- a/crates/ruff_linter/src/rules/fastapi/mod.rs +++ b/crates/ruff_linter/src/rules/fastapi/mod.rs @@ -36,7 +36,7 @@ mod tests { let diagnostics = test_path( Path::new("fastapi").join(path).as_path(), &settings::LinterSettings { - target_version: ruff_python_ast::PythonVersion::PY38, + unresolved_target_version: ruff_python_ast::PythonVersion::PY38, ..settings::LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_linter/src/rules/flake8_annotations/mod.rs b/crates/ruff_linter/src/rules/flake8_annotations/mod.rs index 6662976857555..2b7860d4a8075 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/mod.rs @@ -128,7 +128,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_annotations/auto_return_type.py"), &LinterSettings { - target_version: PythonVersion::PY38, + unresolved_target_version: PythonVersion::PY38, ..LinterSettings::for_rules(vec![ Rule::MissingReturnTypeUndocumentedPublicFunction, Rule::MissingReturnTypePrivateFunction, diff --git a/crates/ruff_linter/src/rules/flake8_async/mod.rs b/crates/ruff_linter/src/rules/flake8_async/mod.rs index e01c3ee49ea3c..165db1d7a4c8e 100644 --- a/crates/ruff_linter/src/rules/flake8_async/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/mod.rs @@ -44,7 +44,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_async").join(path), &LinterSettings { - target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310, ..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout) }, )?; diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index d7dd402402e05..18c4bcf5dfe6a 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -100,7 +100,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_bugbear").join(path).as_path(), &LinterSettings { - target_version, + unresolved_target_version, ..LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs index f0ba88562a652..aedec5cb231f9 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs @@ -217,7 +217,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_builtins").join(path).as_path(), &LinterSettings { - target_version: PythonVersion::PY38, + unresolved_target_version: PythonVersion::PY38, ..LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs index 8202840f0e0f6..d3e3a7b4eeb7e 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs @@ -30,7 +30,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), &settings::LinterSettings { - target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37, ..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation) }, )?; @@ -49,7 +49,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), &settings::LinterSettings { - target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37, ..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) }, )?; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs index cd3e572d103b2..c6edcd739dac9 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs @@ -189,7 +189,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_pyi").join(path).as_path(), &settings::LinterSettings { - target_version: PythonVersion::PY38, + unresolved_target_version: PythonVersion::PY38, ..settings::LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index 18db7f9ff71af..dfb6ce81ddad9 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -92,7 +92,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { - target_version: PythonVersion::PY39, + unresolved_target_version: PythonVersion::PY39, ..settings::LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_linter/src/rules/perflint/mod.rs b/crates/ruff_linter/src/rules/perflint/mod.rs index f6d5bc5e34b47..07d67d8363bfd 100644 --- a/crates/ruff_linter/src/rules/perflint/mod.rs +++ b/crates/ruff_linter/src/rules/perflint/mod.rs @@ -43,7 +43,7 @@ mod tests { Path::new("perflint").join(path).as_path(), &LinterSettings { preview: PreviewMode::Enabled, - target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310, ..LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 0108e640ed0e7..83f0387b9666c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -218,7 +218,7 @@ mod tests { let diagnostics = test_snippet( "PythonFinalizationError", &LinterSettings { - target_version: ruff_python_ast::PythonVersion::PY312, + unresolved_target_version: ruff_python_ast::PythonVersion::PY312, ..LinterSettings::for_rule(Rule::UndefinedName) }, ); diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index aeb694414023c..87b2434c881a6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -156,7 +156,7 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/UP041.py"), &settings::LinterSettings { - target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310, ..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias) }, )?; @@ -169,7 +169,7 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/UP040.py"), &settings::LinterSettings { - target_version: PythonVersion::PY311, + unresolved_target_version: PythonVersion::PY311, ..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) }, )?; @@ -185,7 +185,7 @@ mod tests { pyupgrade: pyupgrade::settings::Settings { keep_runtime_typing: true, }, - target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37, ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; @@ -201,7 +201,7 @@ mod tests { pyupgrade: pyupgrade::settings::Settings { keep_runtime_typing: true, }, - target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310, ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; @@ -214,7 +214,7 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37, ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; @@ -227,7 +227,7 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310, ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; @@ -240,7 +240,7 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37, ..settings::LinterSettings::for_rules([ Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationOptional, @@ -256,7 +256,7 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310, ..settings::LinterSettings::for_rules([ Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationOptional, @@ -272,7 +272,7 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/UP017.py"), &settings::LinterSettings { - target_version: PythonVersion::PY311, + unresolved_target_version: PythonVersion::PY311, ..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC) }, )?; @@ -286,7 +286,7 @@ mod tests { Path::new("pyupgrade/UP044.py"), &settings::LinterSettings { preview: PreviewMode::Enabled, - target_version: PythonVersion::PY311, + unresolved_target_version: PythonVersion::PY311, ..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack) }, )?; diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index df6e363d69069..2513a8e0a76ac 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -129,7 +129,7 @@ mod tests { extend_markup_names: vec![], allowed_markup_calls: vec![], }, - target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310, ..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript) }, )?; diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 202203701aad6..65b500b67d1a1 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -220,7 +220,16 @@ pub struct LinterSettings { pub per_file_ignores: CompiledPerFileIgnoreList, pub fix_safety: FixSafetyTable, - pub target_version: PythonVersion, + /// The non-path-resolved Python version specified by the `target-version` input option. + /// + /// See [`LinterSettings::resolve_target_version`] for a way to obtain the Python version for a + /// given file, while respecting the overrides in `per_file_target_version`. + pub unresolved_target_version: PythonVersion, + /// Path-specific overrides to `unresolved_target_version`. + /// + /// See [`LinterSettings::resolve_target_version`] for a way to check a given [`Path`] + /// against these patterns, while falling back to `unresolved_target_version` if none of them + /// match. pub per_file_target_version: CompiledPerFileVersionList, pub preview: PreviewMode, pub explicit_preview_rules: bool, @@ -283,7 +292,7 @@ impl Display for LinterSettings { self.per_file_ignores, self.fix_safety | nested, - self.target_version, + self.unresolved_target_version, self.per_file_target_version, self.preview, self.explicit_preview_rules, @@ -364,7 +373,7 @@ impl LinterSettings { pub fn for_rule(rule_code: Rule) -> Self { Self { rules: RuleTable::from_iter([rule_code]), - target_version: PythonVersion::latest(), + unresolved_target_version: PythonVersion::latest(), ..Self::default() } } @@ -372,7 +381,7 @@ impl LinterSettings { pub fn for_rules(rules: impl IntoIterator) -> Self { Self { rules: RuleTable::from_iter(rules), - target_version: PythonVersion::latest(), + unresolved_target_version: PythonVersion::latest(), ..Self::default() } } @@ -380,7 +389,7 @@ impl LinterSettings { pub fn new(project_root: &Path) -> Self { Self { exclude: FilePatternSet::default(), - target_version: PythonVersion::default(), + unresolved_target_version: PythonVersion::default(), per_file_target_version: CompiledPerFileVersionList::default(), project_root: project_root.to_path_buf(), rules: DEFAULT_SELECTORS @@ -443,7 +452,7 @@ impl LinterSettings { #[must_use] pub fn with_target_version(mut self, target_version: PythonVersion) -> Self { - self.target_version = target_version; + self.unresolved_target_version = target_version; self } @@ -455,7 +464,7 @@ impl LinterSettings { pub fn resolve_target_version(&self, path: &Path) -> PythonVersion { self.per_file_target_version .is_match(path) - .unwrap_or(self.target_version) + .unwrap_or(self.unresolved_target_version) } } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 799781faf66f8..25bb24c55952d 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -184,7 +184,7 @@ impl Configuration { exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, extension: self.extension.clone().unwrap_or_default(), preview: format_preview, - target_version, + unresolved_target_version: target_version, per_file_target_version: per_file_target_version.clone(), line_width: self .line_length @@ -285,7 +285,7 @@ impl Configuration { exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?, extension: self.extension.unwrap_or_default(), preview: lint_preview, - target_version, + unresolved_target_version: target_version, per_file_target_version, project_root: project_root.to_path_buf(), allowed_confusables: lint diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index f0f7cbfd2edb0..7887edf29a3a3 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -165,7 +165,16 @@ pub struct FormatterSettings { pub exclude: FilePatternSet, pub extension: ExtensionMapping, pub preview: PreviewMode, - pub target_version: PythonVersion, + /// The non-path-resolved Python version specified by the `target-version` input option. + /// + /// See [`FormatterSettings::resolve_target_version`] for a way to obtain the Python version for + /// a given file, while respecting the overrides in `per_file_target_version`. + pub unresolved_target_version: PythonVersion, + /// Path-specific overrides to `unresolved_target_version`. + /// + /// See [`FormatterSettings::resolve_target_version`] for a way to check a given [`Path`] + /// against these patterns, while falling back to `unresolved_target_version` if none of them + /// match. pub per_file_target_version: CompiledPerFileVersionList, pub line_width: LineWidth, @@ -207,7 +216,7 @@ impl FormatterSettings { }; PyFormatOptions::from_source_type(source_type) - .with_target_version(self.target_version) + .with_target_version(self.unresolved_target_version) .with_indent_style(self.indent_style) .with_indent_width(self.indent_width) .with_quote_style(self.quote_style) @@ -227,7 +236,7 @@ impl FormatterSettings { pub fn resolve_target_version(&self, path: &Path) -> PythonVersion { self.per_file_target_version .is_match(path) - .unwrap_or(self.target_version) + .unwrap_or(self.unresolved_target_version) } } @@ -238,7 +247,7 @@ impl Default for FormatterSettings { Self { exclude: FilePatternSet::default(), extension: ExtensionMapping::default(), - target_version: default_options.target_version(), + unresolved_target_version: default_options.target_version(), per_file_target_version: CompiledPerFileVersionList::default(), preview: PreviewMode::Disabled, line_width: default_options.line_width(), @@ -261,7 +270,7 @@ impl fmt::Display for FormatterSettings { namespace = "formatter", fields = [ self.exclude, - self.target_version, + self.unresolved_target_version, self.per_file_target_version, self.preview, self.line_width, From d9d6022d58739c39ddb56aa62f73bfe197e8f76a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 14:16:20 -0500 Subject: [PATCH 27/33] pass Option<&Path> to to_format_options --- crates/ruff/src/commands/format.rs | 14 ++------------ crates/ruff_linter/src/rules/flake8_bugbear/mod.rs | 2 +- crates/ruff_server/src/format.rs | 10 ++++++++-- .../ruff_server/src/server/api/requests/format.rs | 14 ++++++++++---- .../src/server/api/requests/format_range.rs | 6 ++++-- crates/ruff_wasm/src/lib.rs | 2 +- crates/ruff_workspace/src/settings.rs | 13 +++++++++++-- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index af55cedc8c79c..1702dcf6f86af 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -339,17 +339,9 @@ pub(crate) fn format_source( settings: &FormatterSettings, range: Option, ) -> Result { - let target_version = if let Some(path) = path { - settings.resolve_target_version(path) - } else { - settings.unresolved_target_version - }; - match &source_kind { SourceKind::Python(unformatted) => { - let options = settings - .to_format_options(source_type, unformatted) - .with_target_version(target_version); + let options = settings.to_format_options(source_type, unformatted, path); let formatted = if let Some(range) = range { let line_index = LineIndex::from_source_text(unformatted); @@ -399,9 +391,7 @@ pub(crate) fn format_source( )); } - let options = settings - .to_format_options(source_type, notebook.source_code()) - .with_target_version(target_version); + let options = settings.to_format_options(source_type, notebook.source_code(), path); let mut output: Option = None; let mut last: Option = None; diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index 18c4bcf5dfe6a..b7c734a589985 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -100,7 +100,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_bugbear").join(path).as_path(), &LinterSettings { - unresolved_target_version, + unresolved_target_version: target_version, ..LinterSettings::for_rule(rule_code) }, )?; diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index a2257ccedfc88..49d56eeb80168 100644 --- a/crates/ruff_server/src/format.rs +++ b/crates/ruff_server/src/format.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use ruff_formatter::PrintedRange; use ruff_python_ast::PySourceType; use ruff_python_formatter::{format_module_source, FormatModuleError}; @@ -10,8 +12,10 @@ pub(crate) fn format( document: &TextDocument, source_type: PySourceType, formatter_settings: &FormatterSettings, + path: Option<&Path>, ) -> crate::Result> { - let format_options = formatter_settings.to_format_options(source_type, document.contents()); + let format_options = + formatter_settings.to_format_options(source_type, document.contents(), path); match format_module_source(document.contents(), format_options) { Ok(formatted) => { let formatted = formatted.into_code(); @@ -36,8 +40,10 @@ pub(crate) fn format_range( source_type: PySourceType, formatter_settings: &FormatterSettings, range: TextRange, + path: Option<&Path>, ) -> crate::Result> { - let format_options = formatter_settings.to_format_options(source_type, document.contents()); + let format_options = + formatter_settings.to_format_options(source_type, document.contents(), path); match ruff_python_formatter::format_range(document.contents(), range, format_options) { Ok(formatted) => { diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 0a42a610d0cd1..e54f02460181f 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -85,9 +85,10 @@ fn format_text_document( let settings = query.settings(); // If the document is excluded, return early. - if let Some(file_path) = query.file_path() { + let file_path = query.file_path(); + if let Some(file_path) = &file_path { if is_document_excluded_for_formatting( - &file_path, + file_path, &settings.file_resolver, &settings.formatter, text_document.language_id(), @@ -97,8 +98,13 @@ fn format_text_document( } let source = text_document.contents(); - let formatted = crate::format::format(text_document, query.source_type(), &settings.formatter) - .with_failure_code(lsp_server::ErrorCode::InternalError)?; + let formatted = crate::format::format( + text_document, + query.source_type(), + &settings.formatter, + file_path.as_deref(), + ) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; let Some(mut formatted) = formatted else { return Ok(None); }; diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs index 8a2a5371b64e7..72edf77e4d28d 100644 --- a/crates/ruff_server/src/server/api/requests/format_range.rs +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -49,9 +49,10 @@ fn format_text_document_range( let settings = query.settings(); // If the document is excluded, return early. - if let Some(file_path) = query.file_path() { + let file_path = query.file_path(); + if let Some(file_path) = &file_path { if is_document_excluded_for_formatting( - &file_path, + file_path, &settings.file_resolver, &settings.formatter, text_document.language_id(), @@ -68,6 +69,7 @@ fn format_text_document_range( query.source_type(), &settings.formatter, range, + file_path.as_deref(), ) .with_failure_code(lsp_server::ErrorCode::InternalError)?; diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index cc1d5bdcabc28..52b25b379425e 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -303,7 +303,7 @@ impl<'a> ParsedModule<'a> { // TODO(konstin): Add an options for py/pyi to the UI (2/2) let options = settings .formatter - .to_format_options(PySourceType::default(), self.source_code) + .to_format_options(PySourceType::default(), self.source_code, None) .with_source_map_generation(SourceMapGeneration::Enabled); format_module_ast( diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 7887edf29a3a3..4f92018e03b1c 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -193,7 +193,16 @@ pub struct FormatterSettings { } impl FormatterSettings { - pub fn to_format_options(&self, source_type: PySourceType, source: &str) -> PyFormatOptions { + pub fn to_format_options( + &self, + source_type: PySourceType, + source: &str, + path: Option<&Path>, + ) -> PyFormatOptions { + let target_version = path + .map(|path| self.resolve_target_version(path)) + .unwrap_or(self.unresolved_target_version); + let line_ending = match self.line_ending { LineEnding::Lf => ruff_formatter::printer::LineEnding::LineFeed, LineEnding::CrLf => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed, @@ -216,7 +225,7 @@ impl FormatterSettings { }; PyFormatOptions::from_source_type(source_type) - .with_target_version(self.unresolved_target_version) + .with_target_version(target_version) .with_indent_style(self.indent_style) .with_indent_width(self.indent_width) .with_quote_style(self.quote_style) From 778c559598a476d05c8019ba50fd4708d518112a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 17:10:42 -0500 Subject: [PATCH 28/33] tidy up --- crates/ruff_linter/src/settings/mod.rs | 6 +++--- crates/ruff_linter/src/settings/types.rs | 12 +++++++----- crates/ruff_workspace/src/configuration.rs | 9 +++++---- crates/ruff_workspace/src/settings.rs | 6 +++--- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 65b500b67d1a1..b1a9b72670517 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -8,7 +8,7 @@ use rustc_hash::FxHashSet; use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; -use types::CompiledPerFileVersionList; +use types::CompiledPerFileTargetVersionList; use crate::codes::RuleCodePrefix; use ruff_macros::CacheKey; @@ -230,7 +230,7 @@ pub struct LinterSettings { /// See [`LinterSettings::resolve_target_version`] for a way to check a given [`Path`] /// against these patterns, while falling back to `unresolved_target_version` if none of them /// match. - pub per_file_target_version: CompiledPerFileVersionList, + pub per_file_target_version: CompiledPerFileTargetVersionList, pub preview: PreviewMode, pub explicit_preview_rules: bool, @@ -390,7 +390,7 @@ impl LinterSettings { Self { exclude: FilePatternSet::default(), unresolved_target_version: PythonVersion::default(), - per_file_target_version: CompiledPerFileVersionList::default(), + per_file_target_version: CompiledPerFileTargetVersionList::default(), project_root: project_root.to_path_buf(), rules: DEFAULT_SELECTORS .iter() diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index efb45ab7f71ad..148059cbc30fe 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -314,6 +314,8 @@ impl PerFile { } /// Per-file ignored linting rules. +/// +/// See [`PerFile`] for details of the representation. #[derive(Debug, Clone)] pub struct PerFileIgnore(PerFile); @@ -780,15 +782,15 @@ impl PerFileTargetVersion { } #[derive(CacheKey, Clone, Debug, Default)] -pub struct CompiledPerFileVersionList(CompiledPerFileList); +pub struct CompiledPerFileTargetVersionList(CompiledPerFileList); -impl CompiledPerFileVersionList { +impl CompiledPerFileTargetVersionList { /// Given a list of [`PerFileTargetVersion`] patterns, create a compiled set of globs. /// /// Returns an error if either of the glob patterns cannot be parsed. - pub fn resolve(per_file_ignores: Vec) -> Result { + pub fn resolve(per_file_versions: Vec) -> Result { Ok(Self(CompiledPerFileList::resolve( - per_file_ignores.into_iter().map(|version| version.0), + per_file_versions.into_iter().map(|version| version.0), )?)) } @@ -797,7 +799,7 @@ impl CompiledPerFileVersionList { } } -impl Display for CompiledPerFileVersionList { +impl Display for CompiledPerFileTargetVersionList { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 25bb24c55952d..dc7e463fa5a4f 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -29,7 +29,7 @@ use ruff_linter::rules::{flake8_import_conventions, isort, pycodestyle}; use ruff_linter::settings::fix_safety_table::FixSafetyTable; use ruff_linter::settings::rule_table::RuleTable; use ruff_linter::settings::types::{ - CompiledPerFileIgnoreList, CompiledPerFileVersionList, ExtensionMapping, FilePattern, + CompiledPerFileIgnoreList, CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode, RequiredVersion, UnsafeFixes, }; @@ -176,9 +176,10 @@ impl Configuration { PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, }; - let per_file_target_version = - CompiledPerFileVersionList::resolve(self.per_file_target_version.unwrap_or_default()) - .context("failed to resolve `per-file-target-version` table")?; + let per_file_target_version = CompiledPerFileTargetVersionList::resolve( + self.per_file_target_version.unwrap_or_default(), + ) + .context("failed to resolve `per-file-target-version` table")?; let formatter = FormatterSettings { exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 4f92018e03b1c..9ca5ef9168205 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -4,7 +4,7 @@ use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth}; use ruff_graph::AnalyzeSettings; use ruff_linter::display_settings; use ruff_linter::settings::types::{ - CompiledPerFileVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, + CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, UnsafeFixes, }; use ruff_linter::settings::LinterSettings; @@ -175,7 +175,7 @@ pub struct FormatterSettings { /// See [`FormatterSettings::resolve_target_version`] for a way to check a given [`Path`] /// against these patterns, while falling back to `unresolved_target_version` if none of them /// match. - pub per_file_target_version: CompiledPerFileVersionList, + pub per_file_target_version: CompiledPerFileTargetVersionList, pub line_width: LineWidth, @@ -257,7 +257,7 @@ impl Default for FormatterSettings { exclude: FilePatternSet::default(), extension: ExtensionMapping::default(), unresolved_target_version: default_options.target_version(), - per_file_target_version: CompiledPerFileVersionList::default(), + per_file_target_version: CompiledPerFileTargetVersionList::default(), preview: PreviewMode::Disabled, line_width: default_options.line_width(), line_ending: LineEnding::Auto, From d4f2bba3e2796ee39121268af3819e15bb0cb019 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 20 Feb 2025 17:42:14 -0500 Subject: [PATCH 29/33] fix ci --- .../snapshots/show_settings__display_default_settings.snap | 4 ++-- crates/ruff_linter/src/settings/types.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index e3656cb65b843..a4669d8b15c28 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -189,7 +189,7 @@ linter.rules.should_fix = [ linter.per_file_ignores = {} linter.safety_table.forced_safe = [] linter.safety_table.forced_unsafe = [] -linter.target_version = 3.7 +linter.unresolved_target_version = 3.7 linter.per_file_target_version = {} linter.preview = disabled linter.explicit_preview_rules = false @@ -374,7 +374,7 @@ linter.ruff.allowed_markup_calls = [] # Formatter Settings formatter.exclude = [] -formatter.target_version = 3.7 +formatter.unresolved_target_version = 3.7 formatter.per_file_target_version = {} formatter.preview = disabled formatter.line_width = 100 diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 148059cbc30fe..a747d21a5e24d 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -628,15 +628,15 @@ pub struct CompiledPerFileList { /// Helper trait for debug labels on [`PerFile`] types. pub trait PerFileKind { - const LABEL: &str; + const LABEL: &'static str; } impl PerFileKind for RuleSet { - const LABEL: &str = "Adding per-file ignores"; + const LABEL: &'static str = "Adding per-file ignores"; } impl PerFileKind for ast::PythonVersion { - const LABEL: &str = "Setting Python version"; + const LABEL: &'static str = "Setting Python version"; } impl CompiledPerFileList { From 80699a1884044206e658f733a063738c0ae1389d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 21 Feb 2025 08:00:54 -0500 Subject: [PATCH 30/33] mention Checker --- crates/ruff_linter/src/settings/mod.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index b1a9b72670517..cbaaa4608af09 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -222,14 +222,18 @@ pub struct LinterSettings { /// The non-path-resolved Python version specified by the `target-version` input option. /// - /// See [`LinterSettings::resolve_target_version`] for a way to obtain the Python version for a - /// given file, while respecting the overrides in `per_file_target_version`. + /// If you have a `Checker` available, see its `target_version` method instead. + /// + /// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to obtain the Python + /// version for a given file, while respecting the overrides in `per_file_target_version`. pub unresolved_target_version: PythonVersion, /// Path-specific overrides to `unresolved_target_version`. /// - /// See [`LinterSettings::resolve_target_version`] for a way to check a given [`Path`] - /// against these patterns, while falling back to `unresolved_target_version` if none of them - /// match. + /// If you have a `Checker` available, see its `target_version` method instead. + /// + /// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to check a given + /// [`Path`] against these patterns, while falling back to `unresolved_target_version` if none + /// of them match. pub per_file_target_version: CompiledPerFileTargetVersionList, pub preview: PreviewMode, pub explicit_preview_rules: bool, From 23f370ecf873d9faa0e9d01b0d95b01b95419a11 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 21 Feb 2025 08:08:05 -0500 Subject: [PATCH 31/33] switch to debug_label --- crates/ruff_linter/src/fs.rs | 5 +++- crates/ruff_linter/src/settings/types.rs | 35 +++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/ruff_linter/src/fs.rs b/crates/ruff_linter/src/fs.rs index 1528d5aec1ac8..985f2e599ada1 100644 --- a/crates/ruff_linter/src/fs.rs +++ b/crates/ruff_linter/src/fs.rs @@ -7,7 +7,10 @@ use crate::settings::types::CompiledPerFileIgnoreList; /// Create a set with codes matching the pattern/code pairs. pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnoreList) -> RuleSet { - ignore_list.iter_matches(path).flatten().collect() + ignore_list + .iter_matches(path, "Adding per-file ignores") + .flatten() + .collect() } /// Convert any path to an absolute path (based on the current working diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index a747d21a5e24d..cda8255ca3ffc 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -626,19 +626,6 @@ pub struct CompiledPerFileList { inner: Vec>, } -/// Helper trait for debug labels on [`PerFile`] types. -pub trait PerFileKind { - const LABEL: &'static str; -} - -impl PerFileKind for RuleSet { - const LABEL: &'static str = "Adding per-file ignores"; -} - -impl PerFileKind for ast::PythonVersion { - const LABEL: &'static str = "Setting Python version"; -} - impl CompiledPerFileList { /// Given a list of [`PerFile`] patterns, create a compiled set of globs. /// @@ -673,8 +660,15 @@ impl CompiledPerFileList { } } -impl CompiledPerFileList { - pub(crate) fn iter_matches<'a, 'p>(&'a self, path: &'p Path) -> impl Iterator +impl CompiledPerFileList { + /// Return an iterator over the entries in `self` that match the input `path`. + /// + /// `debug_label` is used for [`debug!`] messages explaining why certain patterns were matched. + pub(crate) fn iter_matches<'a, 'p>( + &'a self, + path: &'p Path, + debug_label: &'static str, + ) -> impl Iterator where 'a: 'p, { @@ -686,7 +680,7 @@ impl CompiledPerFileList { } else { debug!( "{} for {:?} due to basename match on {:?}: {:?}", - T::LABEL, + debug_label, path, entry.basename_matcher.glob().regex(), entry.data @@ -699,7 +693,7 @@ impl CompiledPerFileList { } else { debug!( "{} for {:?} due to absolute match on {:?}: {:?}", - T::LABEL, + debug_label, path, entry.absolute_matcher.glob().regex(), entry.data @@ -709,7 +703,7 @@ impl CompiledPerFileList { } else if entry.negated { debug!( "{} for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}", - T::LABEL, + debug_label, path, entry.basename_matcher.glob().regex(), entry.absolute_matcher.glob().regex(), @@ -795,7 +789,10 @@ impl CompiledPerFileTargetVersionList { } pub fn is_match(&self, path: &Path) -> Option { - self.0.iter_matches(path).next().copied() + self.0 + .iter_matches(path, "Setting Python version") + .next() + .copied() } } From 375fd5ccddcb29e17441a3b64b76ae9405386e27 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 21 Feb 2025 08:27:49 -0500 Subject: [PATCH 32/33] impl CacheKey manually to avoid constraints on structs --- crates/ruff_linter/src/settings/types.rs | 39 ++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index cda8255ca3ffc..5aa3bca5abd24 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -578,15 +578,15 @@ impl Display for RequiredVersion { pub type IdentifierPattern = glob::Pattern; /// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage. -#[derive(Debug, Clone, CacheKey)] -pub struct CompiledPerFile { +#[derive(Debug, Clone)] +pub struct CompiledPerFile { pub absolute_matcher: GlobMatcher, pub basename_matcher: GlobMatcher, pub negated: bool, pub data: T, } -impl CompiledPerFile { +impl CompiledPerFile { fn new( absolute_matcher: GlobMatcher, basename_matcher: GlobMatcher, @@ -602,9 +602,21 @@ impl CompiledPerFile { } } +impl CacheKey for CompiledPerFile +where + T: CacheKey, +{ + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.absolute_matcher.cache_key(state); + self.basename_matcher.cache_key(state); + self.negated.cache_key(state); + self.data.cache_key(state); + } +} + impl Display for CompiledPerFile where - T: Display + CacheKey, + T: Display, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { display_settings! { @@ -621,12 +633,21 @@ where } /// A sequence of [`CompiledPerFile`]. -#[derive(Debug, Clone, CacheKey, Default)] -pub struct CompiledPerFileList { +#[derive(Debug, Clone, Default)] +pub struct CompiledPerFileList { inner: Vec>, } -impl CompiledPerFileList { +impl CacheKey for CompiledPerFileList +where + T: CacheKey, +{ + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.inner.cache_key(state); + } +} + +impl CompiledPerFileList { /// Given a list of [`PerFile`] patterns, create a compiled set of globs. /// /// Returns an error if either of the glob patterns cannot be parsed. @@ -660,7 +681,7 @@ impl CompiledPerFileList { } } -impl CompiledPerFileList { +impl CompiledPerFileList { /// Return an iterator over the entries in `self` that match the input `path`. /// /// `debug_label` is used for [`debug!`] messages explaining why certain patterns were matched. @@ -719,7 +740,7 @@ impl CompiledPerFileList { impl Display for CompiledPerFileList where - T: Display + CacheKey, + T: Display, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.inner.is_empty() { From 34345de972e81152f5d8856b79bb4234b488900d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 21 Feb 2025 09:27:58 -0500 Subject: [PATCH 33/33] test script formatting in the server --- crates/ruff_server/src/format.rs | 143 +++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index 49d56eeb80168..4a43de394b914 100644 --- a/crates/ruff_server/src/format.rs +++ b/crates/ruff_server/src/format.rs @@ -62,3 +62,146 @@ pub(crate) fn format_range( Err(err) => Err(err.into()), } } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use insta::assert_snapshot; + use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion}; + use ruff_python_ast::{PySourceType, PythonVersion}; + use ruff_text_size::{TextRange, TextSize}; + use ruff_workspace::FormatterSettings; + + use crate::format::{format, format_range}; + use crate::TextDocument; + + #[test] + fn format_per_file_version() { + let document = TextDocument::new(r#" +with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz: + pass +"#.to_string(), 0); + let per_file_target_version = + CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new( + "test.py".to_string(), + PythonVersion::PY310, + Some(Path::new(".")), + )]) + .unwrap(); + let result = format( + &document, + PySourceType::Python, + &FormatterSettings { + unresolved_target_version: PythonVersion::PY38, + per_file_target_version, + ..Default::default() + }, + Some(Path::new("test.py")), + ) + .expect("Expected no errors when formatting") + .expect("Expected formatting changes"); + + assert_snapshot!(result, @r#" + with ( + open("a_really_long_foo") as foo, + open("a_really_long_bar") as bar, + open("a_really_long_baz") as baz, + ): + pass + "#); + + // same as above but without the per_file_target_version override + let result = format( + &document, + PySourceType::Python, + &FormatterSettings { + unresolved_target_version: PythonVersion::PY38, + ..Default::default() + }, + Some(Path::new("test.py")), + ) + .expect("Expected no errors when formatting") + .expect("Expected formatting changes"); + + assert_snapshot!(result, @r#" + with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open( + "a_really_long_baz" + ) as baz: + pass + "#); + } + + #[test] + fn format_per_file_version_range() -> anyhow::Result<()> { + // prepare a document with formatting changes before and after the intended range (the + // context manager) + let document = TextDocument::new(r#" +def fn(x: str) -> Foo | Bar: return foobar(x) + +with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz: + pass + +sys.exit( +1 +) +"#.to_string(), 0); + + let start = document.contents().find("with").unwrap(); + let end = document.contents().find("pass").unwrap() + "pass".len(); + let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?); + + let per_file_target_version = + CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new( + "test.py".to_string(), + PythonVersion::PY310, + Some(Path::new(".")), + )]) + .unwrap(); + let result = format_range( + &document, + PySourceType::Python, + &FormatterSettings { + unresolved_target_version: PythonVersion::PY38, + per_file_target_version, + ..Default::default() + }, + range, + Some(Path::new("test.py")), + ) + .expect("Expected no errors when formatting") + .expect("Expected formatting changes"); + + assert_snapshot!(result.as_code(), @r#" + with ( + open("a_really_long_foo") as foo, + open("a_really_long_bar") as bar, + open("a_really_long_baz") as baz, + ): + pass + "#); + + // same as above but without the per_file_target_version override + let result = format_range( + &document, + PySourceType::Python, + &FormatterSettings { + unresolved_target_version: PythonVersion::PY38, + ..Default::default() + }, + range, + Some(Path::new("test.py")), + ) + .expect("Expected no errors when formatting") + .expect("Expected formatting changes"); + + assert_snapshot!(result.as_code(), @r#" + with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open( + "a_really_long_baz" + ) as baz: + pass + "#); + + Ok(()) + } +}