From e7a6c19e3a2c88d2eaad88a311c60fbd4e733c14 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:47:13 -0500 Subject: [PATCH] Add `per-file-target-version` option (#16257) ## Summary This PR is another step in preparing to detect syntax errors in the parser. It introduces the new `per-file-target-version` top-level configuration option, which holds a mapping of compiled glob patterns to Python versions. I intend to use the `LinterSettings::resolve_target_version` method here to pass to the parser: https://github.com/astral-sh/ruff/blob/f50849aeef51a381af6c27df8595ac0e1ef5a891/crates/ruff_linter/src/linter.rs#L491-L493 ## Test Plan I added two new CLI tests to show that the `per-file-target-version` is respected in both the formatter and the linter. --- crates/ruff/src/commands/format.rs | 4 +- crates/ruff/tests/format.rs | 47 +++ crates/ruff/tests/lint.rs | 60 ++++ ...ow_settings__display_default_settings.snap | 6 +- .../src/checkers/ast/analyze/expression.rs | 40 +-- .../src/checkers/ast/analyze/statement.rs | 18 +- crates/ruff_linter/src/checkers/ast/mod.rs | 20 +- crates/ruff_linter/src/checkers/filesystem.rs | 4 +- crates/ruff_linter/src/checkers/imports.rs | 4 +- crates/ruff_linter/src/fs.rs | 38 +-- crates/ruff_linter/src/linter.rs | 5 + crates/ruff_linter/src/renamer.rs | 2 +- crates/ruff_linter/src/rules/fastapi/mod.rs | 2 +- .../rules/fastapi_non_annotated_dependency.rs | 4 +- .../src/rules/flake8_annotations/mod.rs | 2 +- .../flake8_annotations/rules/definition.rs | 12 +- .../ruff_linter/src/rules/flake8_async/mod.rs | 2 +- .../rules/async_function_with_timeout.rs | 2 +- .../src/rules/flake8_bugbear/mod.rs | 2 +- .../rules/batched_without_explicit_strict.rs | 2 +- .../rules/class_as_data_structure.rs | 2 +- .../src/rules/flake8_builtins/mod.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 | 9 +- .../rules/flake8_future_annotations/mod.rs | 4 +- .../ruff_linter/src/rules/flake8_pyi/mod.rs | 2 +- .../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 +- .../src/rules/flake8_type_checking/mod.rs | 2 +- .../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 +- crates/ruff_linter/src/rules/perflint/mod.rs | 2 +- .../perflint/rules/try_except_in_loop.rs | 2 +- crates/ruff_linter/src/rules/pyflakes/mod.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 +- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 20 +- .../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 +- crates/ruff_linter/src/rules/ruff/mod.rs | 2 +- .../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 | 40 ++- crates/ruff_linter/src/settings/types.rs | 292 ++++++++++++++---- crates/ruff_server/src/format.rs | 153 ++++++++- .../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/configuration.rs | 34 +- crates/ruff_workspace/src/options.rs | 23 ++ crates/ruff_workspace/src/settings.rs | 47 ++- ruff.schema.json | 10 + 78 files changed, 820 insertions(+), 274 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 5a1c4f9787..1702dcf6f8 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -341,7 +341,7 @@ pub(crate) fn format_source( ) -> Result { match &source_kind { SourceKind::Python(unformatted) => { - let options = settings.to_format_options(source_type, unformatted); + 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); @@ -391,7 +391,7 @@ 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(), path); let mut output: Option = None; let mut last: Option = None; diff --git a/crates/ruff/tests/format.rs b/crates/ruff/tests/format.rs index a24b4e313f..122980d5ed 100644 --- a/crates/ruff/tests/format.rs +++ b/crates/ruff/tests/format.rs @@ -2086,3 +2086,50 @@ 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() { + // 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"}"#]) + .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 ----- + "#); +} diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index a082ab7cbb..e3348cb22f 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -2567,3 +2567,63 @@ fn a005_module_shadowing_strict_default() -> Result<()> { }); Ok(()) } + +/// 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"]) + .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#]) + .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: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); +} 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 e0bb260b9e..a4669d8b15 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,8 @@ 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 linter.extension = ExtensionMapping({}) @@ -373,7 +374,8 @@ 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 formatter.line_ending = auto diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 7795d8c1ac..3c386f7ea0 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 4b5594e47f..77863086d6 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 e09405250f..c4f98433bd 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}; @@ -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> { @@ -242,6 +244,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() { @@ -272,6 +275,7 @@ impl<'a> Checker<'a> { notebook_index, last_stmt_end: TextSize::default(), docstring_state: DocstringState::default(), + target_version, } } } @@ -500,6 +504,11 @@ impl<'a> Checker<'a> { self.report_diagnostic(diagnostic); } } + + /// Return the [`PythonVersion`] to use for version-related checks. + pub(crate) const fn target_version(&self) -> PythonVersion { + self.target_version + } } impl<'a> Visitor<'a> for Checker<'a> { @@ -2108,17 +2117,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 +2670,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 +2710,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/filesystem.rs b/crates/ruff_linter/src/checkers/filesystem.rs index 98ee3b80c3..f6d9c491c8 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/checkers/imports.rs b/crates/ruff_linter/src/checkers/imports.rs index 7b3f1462c2..2dd7a33a29 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/fs.rs b/crates/ruff_linter/src/fs.rs index 39a09b2ed1..985f2e599a 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,43 +7,8 @@ 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.rules - ); - Some(&entry.rules) - } - } 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.rules - ); - Some(&entry.rules) - } - } 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.rules - ); - Some(&entry.rules) - } else { - None - } - }) + .iter_matches(path, "Adding per-file ignores") .flatten() .collect() } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index ed27a6fdfd..4179c3b381 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, )); } @@ -158,6 +161,7 @@ pub fn check_path( source_type, cell_offsets, notebook_index, + target_version, )); } if use_imports { @@ -171,6 +175,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 1b6c5835ec..abf3434cde 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/mod.rs b/crates/ruff_linter/src/rules/fastapi/mod.rs index 91e77e6a5c..70d22c5020 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/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index b393816140..ec0e5e33a1 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/mod.rs b/crates/ruff_linter/src/rules/flake8_annotations/mod.rs index 6662976857..2b7860d4a8 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_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index e637f0ea13..56942616e7 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/mod.rs b/crates/ruff_linter/src/rules/flake8_async/mod.rs index e01c3ee49e..165db1d7a4 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_async/rules/async_function_with_timeout.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs index b588cf9e21..53cbf952b1 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/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index d7dd402402..b7c734a589 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: target_version, ..LinterSettings::for_rule(rule_code) }, )?; 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 8b12f6e9ee..0fcdb5c8d0 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 774a25b6da..01bbc446e7 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/mod.rs b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs index f0ba88562a..aedec5cb23 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_builtins/rules/builtin_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs index 8683222d02..b0d5014de9 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 c151c117ca..79f0f0ded3 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 bdebcbf035..83332397b4 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 28d8461ed5..1e778fa0ce 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 ecd919327c..b77b3e0ee6 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 a74d4c948d..95f5c00611 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; @@ -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,7 +99,7 @@ pub(crate) fn stdlib_module_shadowing( let module_name = components.next()?; - if is_allowed_module(settings, &module_name) { + if is_allowed_module(settings, target_version, &module_name) { return None; } @@ -129,7 +130,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, 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("__") { @@ -145,5 +146,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(version.minor, module) } 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 8202840f0e..d3e3a7b4ee 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 cd3e572d10..c6edcd739d 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_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index a3de847437..8d89b29a82 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 993419a49f..5500f706a3 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 4364a9d4f9..1675922077 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 c1427deec6..46970f64f8 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 b9e279e5e8..9432521da6 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 2a3ab2f6f0..3f0577928f 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 55df17fd4f..91ec9c16b2 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/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index 18db7f9ff7..dfb6ce81dd 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/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index c8f80c500a..245741b9e9 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 ad873a8534..777a7b1237 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 feb7437d2f..1750f41de8 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 fb1fb328f1..4ea44ee823 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/mod.rs b/crates/ruff_linter/src/rules/perflint/mod.rs index f6d5bc5e34..07d67d8363 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/perflint/rules/try_except_in_loop.rs b/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs index 1ef5dd258f..aa53fb5c8e 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/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 0108e640ed..83f0387b96 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/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index 13b3bb333a..1716b84b7f 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 9dfe324a75..07dd4f8470 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 573e6b5c04..e8709de615 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 a5261851ca..8ed8d8b515 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/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index aeb6944140..87b2434c88 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/pyupgrade/rules/deprecated_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs index 9c83e3cede..05d00bd6f6 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 6b031396f6..969220eb44 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 ae11deb9ea..24f365a2b6 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 197eeae77f..b7d0c99de8 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 d7dd9c8d04..5895b3e8e9 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 159f1a1c3a..b1e030f77e 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 754bc5a31d..c9d9af485f 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 3242f2f24f..22d27551df 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 c2e59836ba..50728df173 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 ba1f51412a..0cd0df8bec 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 6779a482a6..010aaaa2b6 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 0466403947..ee28e60391 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 4623c008c3..1314978dc5 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 e35320d7cc..fa6ac1580c 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/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index df6e363d69..2513a8e0a7 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/rules/ruff/rules/class_with_mixed_type_vars.rs b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs index 846ff0b109..cce3d70a0a 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 de90e3b6e8..adaec2f99f 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 01f2dd9d0c..a582cd3c4b 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 01f9f7220f..cbaaa4608a 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -8,6 +8,7 @@ use rustc_hash::FxHashSet; use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; +use types::CompiledPerFileTargetVersionList; use crate::codes::RuleCodePrefix; use ruff_macros::CacheKey; @@ -219,7 +220,21 @@ 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. + /// + /// 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`. + /// + /// 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, @@ -281,7 +296,8 @@ 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, self.extension | debug, @@ -361,7 +377,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() } } @@ -369,7 +385,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() } } @@ -377,7 +393,8 @@ 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: CompiledPerFileTargetVersionList::default(), project_root: project_root.to_path_buf(), rules: DEFAULT_SELECTORS .iter() @@ -439,9 +456,20 @@ 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 } + + /// 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 { + self.per_file_target_version + .is_match(path) + .unwrap_or(self.unresolved_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 007a4a4e2b..5aa3bca5ab 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -5,8 +5,9 @@ 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}; use rustc_hash::FxHashMap; use serde::{de, Deserialize, Deserializer, Serialize}; @@ -274,22 +275,25 @@ impl CacheKey for FilePatternSet { } } +/// A glob pattern and associated data for matching file paths. #[derive(Debug, Clone)] -pub struct PerFileIgnore { - pub(crate) basename: String, - pub(crate) absolute: PathBuf, - pub(crate) negated: bool, - pub(crate) rules: RuleSet, +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 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 { + /// 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 { pattern.drain(..1); @@ -304,11 +308,26 @@ impl PerFileIgnore { basename: pattern, absolute, negated, - rules, + data, } } } +/// Per-file ignored linting rules. +/// +/// See [`PerFile`] for details of the representation. +#[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, @@ -558,15 +577,47 @@ impl Display for RequiredVersion { /// pattern matching. pub type IdentifierPattern = glob::Pattern; -#[derive(Debug, Clone, CacheKey)] -pub struct CompiledPerFileIgnore { +/// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage. +#[derive(Debug, Clone)] +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 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, +{ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { display_settings! { formatter = f, @@ -574,52 +625,130 @@ impl Display for CompiledPerFileIgnore { self.absolute_matcher | globmatcher, self.basename_matcher | globmatcher, self.negated, - self.rules, + self.data, ] } Ok(()) } } -#[derive(Debug, Clone, CacheKey, Default)] -pub struct CompiledPerFileIgnoreList { - // Ordered as (absolute path matcher, basename matcher, rules) - ignores: Vec, +/// A sequence of [`CompiledPerFile`]. +#[derive(Debug, Clone, Default)] +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 - .into_iter() - .map(|per_file_ignore| { - // Construct absolute path matcher. - let absolute_matcher = - Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher(); - - // Construct basename matcher. - let basename_matcher = Glob::new(&per_file_ignore.basename)?.compile_matcher(); - - Ok(CompiledPerFileIgnore { - absolute_matcher, - basename_matcher, - negated: per_file_ignore.negated, - rules: per_file_ignore.rules, - }) - }) - .collect(); - Ok(Self { ignores: ignores? }) +impl CacheKey for CompiledPerFileList +where + T: CacheKey, +{ + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.inner.cache_key(state); } } -impl Display for CompiledPerFileIgnoreList { +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()) + .with_context(|| format!("invalid glob {:?}", per_file_ignore.absolute))? + .compile_matcher(); + + // Construct basename 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, + basename_matcher, + per_file_ignore.negated, + per_file_ignore.data, + )) + }) + .collect(); + Ok(Self { inner: inner? }) + } + + pub(crate) fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} + +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, + { + 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 { + debug!( + "{} for {:?} due to basename match on {:?}: {:?}", + debug_label, + 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!( + "{} for {:?} due to absolute match on {:?}: {:?}", + debug_label, + path, + entry.absolute_matcher.glob().regex(), + entry.data + ); + Some(&entry.data) + } + } else if entry.negated { + debug!( + "{} for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}", + debug_label, + path, + entry.basename_matcher.glob().regex(), + entry.absolute_matcher.glob().regex(), + entry.data + ); + Some(&entry.data) + } else { + None + } + }) + } +} + +impl Display for CompiledPerFileList +where + T: Display, +{ 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}")?; + for value in &self.inner { + writeln!(f, "\t{value}")?; } write!(f, "}}")?; } @@ -627,11 +756,70 @@ 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) + } +} + +/// Contains the target Python version for a given glob pattern. +/// +/// See [`PerFile`] for details of the representation. +#[derive(Debug, Clone)] +pub struct PerFileTargetVersion(PerFile); + +impl PerFileTargetVersion { + pub fn new(pattern: String, version: ast::PythonVersion, project_root: Option<&Path>) -> Self { + Self(PerFile::new(pattern, project_root, version)) + } +} + +#[derive(CacheKey, Clone, Debug, Default)] +pub struct CompiledPerFileTargetVersionList(CompiledPerFileList); + +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_versions: Vec) -> Result { + Ok(Self(CompiledPerFileList::resolve( + per_file_versions.into_iter().map(|version| version.0), + )?)) + } + + pub fn is_match(&self, path: &Path) -> Option { + self.0 + .iter_matches(path, "Setting Python version") + .next() + .copied() + } +} + +impl Display for CompiledPerFileTargetVersionList { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) } } diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index a2257ccedf..4a43de394b 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) => { @@ -56,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(()) + } +} diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 0a42a610d0..e54f024601 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 8a2a5371b6..72edf77e4d 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 cc1d5bdcab..52b25b3794 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/configuration.rs b/crates/ruff_workspace/src/configuration.rs index f0c8643164..dc7e463fa5 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; @@ -29,8 +29,9 @@ 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, CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, + FilePatternSet, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode, + RequiredVersion, UnsafeFixes, }; use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS}; use ruff_linter::{ @@ -138,6 +139,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, @@ -174,11 +176,17 @@ impl Configuration { PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, }; + 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())?, 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 .map_or(format_defaults.line_width, |length| { @@ -278,7 +286,8 @@ 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 .allowed_confusables @@ -533,6 +542,18 @@ impl Configuration { .map(|src| resolve_src(&src, project_root)) .transpose()?, target_version: options.target_version.map(ast::PythonVersion::from), + per_file_target_version: options.per_file_target_version.map(|versions| { + versions + .into_iter() + .map(|(pattern, version)| { + PerFileTargetVersion::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, @@ -580,6 +601,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 88994f6ecc..01d9ada89c 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -333,6 +333,29 @@ pub struct Options { )] pub target_version: Option, + /// 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]", + 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/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 0ef15d4407..9ca5ef9168 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, + CompiledPerFileTargetVersionList, 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,17 @@ pub struct FormatterSettings { pub exclude: FilePatternSet, pub extension: ExtensionMapping, pub preview: PreviewMode, - pub target_version: ruff_python_ast::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: CompiledPerFileTargetVersionList, pub line_width: LineWidth, @@ -182,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, @@ -205,7 +225,7 @@ impl FormatterSettings { }; PyFormatOptions::from_source_type(source_type) - .with_target_version(self.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) @@ -216,6 +236,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.unresolved_target_version) + } } impl Default for FormatterSettings { @@ -225,7 +256,8 @@ 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: CompiledPerFileTargetVersionList::default(), preview: PreviewMode::Disabled, line_width: default_options.line_width(), line_ending: LineEnding::Auto, @@ -247,7 +279,8 @@ 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, self.line_ending, diff --git a/ruff.schema.json b/ruff.schema.json index 9cac3857ad..57dd60a7c4 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": [