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:


f50849aeef/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.
This commit is contained in:
Brent Westbrook 2025-02-24 08:47:13 -05:00 committed by GitHub
parent 42a5f5ef6a
commit e7a6c19e3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 820 additions and 274 deletions

View file

@ -341,7 +341,7 @@ pub(crate) fn format_source(
) -> Result<FormattedSource, FormatCommandError> { ) -> Result<FormattedSource, FormatCommandError> {
match &source_kind { match &source_kind {
SourceKind::Python(unformatted) => { 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 formatted = if let Some(range) = range {
let line_index = LineIndex::from_source_text(unformatted); 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<String> = None; let mut output: Option<String> = None;
let mut last: Option<TextSize> = None; let mut last: Option<TextSize> = None;

View file

@ -2086,3 +2086,50 @@ fn range_formatting_notebook() {
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks. 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 <https://github.com/python/cpython/issues/56991#issuecomment-1093555135>
#[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 -----
"#);
}

View file

@ -2567,3 +2567,63 @@ fn a005_module_shadowing_strict_default() -> Result<()> {
}); });
Ok(()) 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 -----
"
);
}

View file

@ -189,7 +189,8 @@ linter.rules.should_fix = [
linter.per_file_ignores = {} linter.per_file_ignores = {}
linter.safety_table.forced_safe = [] linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = [] 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.preview = disabled
linter.explicit_preview_rules = false linter.explicit_preview_rules = false
linter.extension = ExtensionMapping({}) linter.extension = ExtensionMapping({})
@ -373,7 +374,8 @@ linter.ruff.allowed_markup_calls = []
# Formatter Settings # Formatter Settings
formatter.exclude = [] formatter.exclude = []
formatter.target_version = 3.7 formatter.unresolved_target_version = 3.7
formatter.per_file_target_version = {}
formatter.preview = disabled formatter.preview = disabled
formatter.line_width = 100 formatter.line_width = 100
formatter.line_ending = auto formatter.line_ending = auto

View file

@ -34,8 +34,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
{ {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub() if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY310 && checker.target_version() < PythonVersion::PY310
&& checker.settings.target_version >= PythonVersion::PY37 && checker.target_version() >= PythonVersion::PY37
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing && !checker.settings.pyupgrade.keep_runtime_typing
{ {
@ -49,8 +49,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::NonPEP604AnnotationOptional, Rule::NonPEP604AnnotationOptional,
]) { ]) {
if checker.source_type.is_stub() if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::PY310 || checker.target_version() >= PythonVersion::PY310
|| (checker.settings.target_version >= PythonVersion::PY37 || (checker.target_version() >= PythonVersion::PY37
&& checker.semantic.future_annotations_or_stub() && checker.semantic.future_annotations_or_stub()
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing) && !checker.settings.pyupgrade.keep_runtime_typing)
@ -64,7 +64,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
// Ex) list[...] // Ex) list[...]
if checker.enabled(Rule::FutureRequiredTypeAnnotation) { if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub() 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_annotation()
&& checker.semantic.in_runtime_evaluated_annotation() && checker.semantic.in_runtime_evaluated_annotation()
&& !checker.semantic.in_string_type_definition() && !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.enabled(Rule::UnnecessaryDefaultTypeArgs) {
if checker.settings.target_version >= PythonVersion::PY313 { if checker.target_version() >= PythonVersion::PY313 {
pyupgrade::rules::unnecessary_default_type_args(checker, expr); 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.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub() if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY39 && checker.target_version() < PythonVersion::PY39
&& checker.settings.target_version >= PythonVersion::PY37 && checker.target_version() >= PythonVersion::PY37
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing && !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.enabled(Rule::NonPEP585Annotation) {
if checker.source_type.is_stub() if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::PY39 || checker.target_version() >= PythonVersion::PY39
|| (checker.settings.target_version >= PythonVersion::PY37 || (checker.target_version() >= PythonVersion::PY37
&& checker.semantic.future_annotations_or_stub() && checker.semantic.future_annotations_or_stub()
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing) && !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 let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub() if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY39 && checker.target_version() < PythonVersion::PY39
&& checker.settings.target_version >= PythonVersion::PY37 && checker.target_version() >= PythonVersion::PY37
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing && !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.enabled(Rule::NonPEP585Annotation) {
if checker.source_type.is_stub() if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::PY39 || checker.target_version() >= PythonVersion::PY39
|| (checker.settings.target_version >= PythonVersion::PY37 || (checker.target_version() >= PythonVersion::PY37
&& checker.semantic.future_annotations_or_stub() && checker.semantic.future_annotations_or_stub()
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing) && !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); refurb::rules::regex_flag_alias(checker, expr);
} }
if checker.enabled(Rule::DatetimeTimezoneUTC) { 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); 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); pyupgrade::rules::os_error_alias_call(checker, func);
} }
if checker.enabled(Rule::TimeoutErrorAlias) { 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); pyupgrade::rules::timeout_error_alias_call(checker, func);
} }
} }
if checker.enabled(Rule::NonPEP604Isinstance) { 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); 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.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); 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); flake8_pytest_style::rules::fail_call(checker, call);
} }
if checker.enabled(Rule::ZipInsteadOfPairwise) { 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); ruff::rules::zip_instead_of_pairwise(checker, call);
} }
} }
@ -1385,7 +1385,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
// Ex) `str | None` // Ex) `str | None`
if checker.enabled(Rule::FutureRequiredTypeAnnotation) { if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub() 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_annotation()
&& checker.semantic.in_runtime_evaluated_annotation() && checker.semantic.in_runtime_evaluated_annotation()
&& !checker.semantic.in_string_type_definition() && !checker.semantic.in_string_type_definition()

View file

@ -164,9 +164,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt); flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
} }
} }
if checker.source_type.is_stub() if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY311 {
|| checker.settings.target_version >= PythonVersion::PY311
{
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) { if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
flake8_pyi::rules::no_return_argument_annotation(checker, parameters); 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); pylint::rules::global_statement(checker, name);
} }
if checker.enabled(Rule::LRUCacheWithoutParameters) { 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); pyupgrade::rules::lru_cache_without_parameters(checker, decorator_list);
} }
} }
if checker.enabled(Rule::LRUCacheWithMaxsizeNone) { 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); 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); pyupgrade::rules::useless_object_inheritance(checker, class_def);
} }
if checker.enabled(Rule::ReplaceStrEnum) { 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); 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.enabled(Rule::UnnecessaryFutureImport) {
if checker.settings.target_version >= PythonVersion::PY37 { if checker.target_version() >= PythonVersion::PY37 {
if let Some("__future__") = module { if let Some("__future__") = module {
pyupgrade::rules::unnecessary_future_import(checker, stmt, names); 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.enabled(Rule::TimeoutErrorAlias) {
if checker.settings.target_version >= PythonVersion::PY310 { if checker.target_version() >= PythonVersion::PY310 {
if let Some(item) = exc { if let Some(item) = exc {
pyupgrade::rules::timeout_error_alias_raise(checker, item); 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); flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
} }
if checker.enabled(Rule::ContinueInFinally) { 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); 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); pyupgrade::rules::os_error_alias_handlers(checker, handlers);
} }
if checker.enabled(Rule::TimeoutErrorAlias) { 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); pyupgrade::rules::timeout_error_alias_handlers(checker, handlers);
} }
} }

View file

@ -38,7 +38,7 @@ use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr, self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern, 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_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Stylist}; use ruff_python_codegen::{Generator, Stylist};
@ -223,6 +223,8 @@ pub(crate) struct Checker<'a> {
last_stmt_end: TextSize, last_stmt_end: TextSize,
/// A state describing if a docstring is expected or not. /// A state describing if a docstring is expected or not.
docstring_state: DocstringState, docstring_state: DocstringState,
/// The target [`PythonVersion`] for version-dependent checks
target_version: PythonVersion,
} }
impl<'a> Checker<'a> { impl<'a> Checker<'a> {
@ -242,6 +244,7 @@ impl<'a> Checker<'a> {
source_type: PySourceType, source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>, cell_offsets: Option<&'a CellOffsets>,
notebook_index: Option<&'a NotebookIndex>, notebook_index: Option<&'a NotebookIndex>,
target_version: PythonVersion,
) -> Checker<'a> { ) -> Checker<'a> {
let mut semantic = SemanticModel::new(&settings.typing_modules, path, module); let mut semantic = SemanticModel::new(&settings.typing_modules, path, module);
if settings.preview.is_enabled() { if settings.preview.is_enabled() {
@ -272,6 +275,7 @@ impl<'a> Checker<'a> {
notebook_index, notebook_index,
last_stmt_end: TextSize::default(), last_stmt_end: TextSize::default(),
docstring_state: DocstringState::default(), docstring_state: DocstringState::default(),
target_version,
} }
} }
} }
@ -500,6 +504,11 @@ impl<'a> Checker<'a> {
self.report_diagnostic(diagnostic); 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> { impl<'a> Visitor<'a> for Checker<'a> {
@ -2108,17 +2117,14 @@ impl<'a> Checker<'a> {
} }
fn bind_builtins(&mut self) { fn bind_builtins(&mut self) {
let target_version = self.target_version();
let mut bind_builtin = |builtin| { let mut bind_builtin = |builtin| {
// Add the builtin to the scope. // Add the builtin to the scope.
let binding_id = self.semantic.push_builtin(); let binding_id = self.semantic.push_builtin();
let scope = self.semantic.global_scope_mut(); let scope = self.semantic.global_scope_mut();
scope.add(builtin, binding_id); scope.add(builtin, binding_id);
}; };
let standard_builtins = python_builtins(target_version.minor, self.source_type.is_ipynb());
let standard_builtins = python_builtins(
self.settings.target_version.minor,
self.source_type.is_ipynb(),
);
for builtin in standard_builtins { for builtin in standard_builtins {
bind_builtin(builtin); bind_builtin(builtin);
} }
@ -2664,6 +2670,7 @@ pub(crate) fn check_ast(
source_type: PySourceType, source_type: PySourceType,
cell_offsets: Option<&CellOffsets>, cell_offsets: Option<&CellOffsets>,
notebook_index: Option<&NotebookIndex>, notebook_index: Option<&NotebookIndex>,
target_version: PythonVersion,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
let module_path = package let module_path = package
.map(PackageRoot::path) .map(PackageRoot::path)
@ -2703,6 +2710,7 @@ pub(crate) fn check_ast(
source_type, source_type,
cell_offsets, cell_offsets,
notebook_index, notebook_index,
target_version,
); );
checker.bind_builtins(); checker.bind_builtins();

View file

@ -1,6 +1,7 @@
use std::path::Path; use std::path::Path;
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_python_ast::PythonVersion;
use ruff_python_trivia::CommentRanges; use ruff_python_trivia::CommentRanges;
use crate::package::PackageRoot; use crate::package::PackageRoot;
@ -17,6 +18,7 @@ pub(crate) fn check_file_path(
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, comment_ranges: &CommentRanges,
settings: &LinterSettings, settings: &LinterSettings,
target_version: PythonVersion,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![]; let mut diagnostics: Vec<Diagnostic> = vec![];
@ -46,7 +48,7 @@ pub(crate) fn check_file_path(
// flake8-builtins // flake8-builtins
if settings.rules.enabled(Rule::StdlibModuleShadowing) { 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); diagnostics.push(diagnostic);
} }
} }

View file

@ -3,7 +3,7 @@
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_notebook::CellOffsets; use ruff_notebook::CellOffsets;
use ruff_python_ast::statement_visitor::StatementVisitor; 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_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::Parsed; use ruff_python_parser::Parsed;
@ -27,6 +27,7 @@ pub(crate) fn check_imports(
package: Option<PackageRoot<'_>>, package: Option<PackageRoot<'_>>,
source_type: PySourceType, source_type: PySourceType,
cell_offsets: Option<&CellOffsets>, cell_offsets: Option<&CellOffsets>,
target_version: PythonVersion,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
// Extract all import blocks from the AST. // Extract all import blocks from the AST.
let tracker = { let tracker = {
@ -52,6 +53,7 @@ pub(crate) fn check_imports(
package, package,
source_type, source_type,
parsed.tokens(), parsed.tokens(),
target_version,
) { ) {
diagnostics.push(diagnostic); diagnostics.push(diagnostic);
} }

View file

@ -1,6 +1,5 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use log::debug;
use path_absolutize::Absolutize; use path_absolutize::Absolutize;
use crate::registry::RuleSet; use crate::registry::RuleSet;
@ -8,43 +7,8 @@ use crate::settings::types::CompiledPerFileIgnoreList;
/// Create a set with codes matching the pattern/code pairs. /// Create a set with codes matching the pattern/code pairs.
pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnoreList) -> RuleSet { 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 ignore_list
.iter() .iter_matches(path, "Adding per-file ignores")
.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
}
})
.flatten() .flatten()
.collect() .collect()
} }

View file

@ -104,6 +104,8 @@ pub fn check_path(
)); ));
} }
let target_version = settings.resolve_target_version(path);
// Run the filesystem-based rules. // Run the filesystem-based rules.
if settings if settings
.rules .rules
@ -116,6 +118,7 @@ pub fn check_path(
locator, locator,
comment_ranges, comment_ranges,
settings, settings,
target_version,
)); ));
} }
@ -158,6 +161,7 @@ pub fn check_path(
source_type, source_type,
cell_offsets, cell_offsets,
notebook_index, notebook_index,
target_version,
)); ));
} }
if use_imports { if use_imports {
@ -171,6 +175,7 @@ pub fn check_path(
package, package,
source_type, source_type,
cell_offsets, cell_offsets,
target_version,
); );
diagnostics.extend(import_diagnostics); diagnostics.extend(import_diagnostics);

View file

@ -399,7 +399,7 @@ impl ShadowedKind {
if is_python_builtin( if is_python_builtin(
new_name, new_name,
checker.settings.target_version.minor, checker.target_version().minor,
checker.source_type.is_ipynb(), checker.source_type.is_ipynb(),
) { ) {
return ShadowedKind::BuiltIn; return ShadowedKind::BuiltIn;

View file

@ -36,7 +36,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(), Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings { &settings::LinterSettings {
target_version: ruff_python_ast::PythonVersion::PY38, unresolved_target_version: ruff_python_ast::PythonVersion::PY38,
..settings::LinterSettings::for_rule(rule_code) ..settings::LinterSettings::for_rule(rule_code)
}, },
)?; )?;

View file

@ -226,13 +226,13 @@ fn create_diagnostic(
) -> bool { ) -> bool {
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
FastApiNonAnnotatedDependency { FastApiNonAnnotatedDependency {
py_version: checker.settings.target_version, py_version: checker.target_version(),
}, },
parameter.range, parameter.range,
); );
let try_generate_fix = || { let try_generate_fix = || {
let module = if checker.settings.target_version >= PythonVersion::PY39 { let module = if checker.target_version() >= PythonVersion::PY39 {
"typing" "typing"
} else { } else {
"typing_extensions" "typing_extensions"

View file

@ -128,7 +128,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_annotations/auto_return_type.py"), Path::new("flake8_annotations/auto_return_type.py"),
&LinterSettings { &LinterSettings {
target_version: PythonVersion::PY38, unresolved_target_version: PythonVersion::PY38,
..LinterSettings::for_rules(vec![ ..LinterSettings::for_rules(vec![
Rule::MissingReturnTypeUndocumentedPublicFunction, Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction, Rule::MissingReturnTypePrivateFunction,

View file

@ -523,7 +523,7 @@ fn check_dynamically_typed<F>(
if type_hint_resolves_to_any( if type_hint_resolves_to_any(
parsed_annotation.expression(), parsed_annotation.expression(),
checker, checker,
checker.settings.target_version, checker.target_version(),
) { ) {
diagnostics.push(Diagnostic::new( diagnostics.push(Diagnostic::new(
AnyType { name: func() }, AnyType { name: func() },
@ -532,7 +532,7 @@ fn check_dynamically_typed<F>(
} }
} }
} else { } 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( diagnostics.push(Diagnostic::new(
AnyType { name: func() }, AnyType { name: func() },
annotation.range(), annotation.range(),
@ -725,7 +725,7 @@ pub(crate) fn definition(
checker.importer(), checker.importer(),
function.parameters.start(), function.parameters.start(),
checker.semantic(), checker.semantic(),
checker.settings.target_version, checker.target_version(),
) )
}) })
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
@ -756,7 +756,7 @@ pub(crate) fn definition(
checker.importer(), checker.importer(),
function.parameters.start(), function.parameters.start(),
checker.semantic(), checker.semantic(),
checker.settings.target_version, checker.target_version(),
) )
}) })
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
@ -826,7 +826,7 @@ pub(crate) fn definition(
checker.importer(), checker.importer(),
function.parameters.start(), function.parameters.start(),
checker.semantic(), checker.semantic(),
checker.settings.target_version, checker.target_version(),
) )
}) })
.map(|(return_type, edits)| { .map(|(return_type, edits)| {
@ -865,7 +865,7 @@ pub(crate) fn definition(
checker.importer(), checker.importer(),
function.parameters.start(), function.parameters.start(),
checker.semantic(), checker.semantic(),
checker.settings.target_version, checker.target_version(),
) )
}) })
.map(|(return_type, edits)| { .map(|(return_type, edits)| {

View file

@ -44,7 +44,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_async").join(path), Path::new("flake8_async").join(path),
&LinterSettings { &LinterSettings {
target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::PY310,
..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout) ..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout)
}, },
)?; )?;

View file

@ -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 // 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; return;
} }

View file

@ -100,7 +100,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(), Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings { &LinterSettings {
target_version, unresolved_target_version: target_version,
..LinterSettings::for_rule(rule_code) ..LinterSettings::for_rule(rule_code)
}, },
)?; )?;

View file

@ -59,7 +59,7 @@ impl Violation for BatchedWithoutExplicitStrict {
/// B911 /// B911
pub(crate) fn batched_without_explicit_strict(checker: &Checker, call: &ExprCall) { 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; return;
} }

View file

@ -78,7 +78,7 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl
// skip `self` // skip `self`
.skip(1) .skip(1)
.all(|param| param.annotation().is_some() && !param.is_variadic()) .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 // `__init__` should not have complicated logic in it
// only assignments // only assignments
&& func_def && func_def

View file

@ -217,7 +217,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_builtins").join(path).as_path(), Path::new("flake8_builtins").join(path).as_path(),
&LinterSettings { &LinterSettings {
target_version: PythonVersion::PY38, unresolved_target_version: PythonVersion::PY38,
..LinterSettings::for_rule(rule_code) ..LinterSettings::for_rule(rule_code)
}, },
)?; )?;

View file

@ -68,7 +68,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &Checker, parameter: &Paramete
parameter.name(), parameter.name(),
checker.source_type, checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist, &checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version, checker.target_version(),
) { ) {
// Ignore parameters in lambda expressions. // Ignore parameters in lambda expressions.
// (That is the domain of A006.) // (That is the domain of A006.)

View file

@ -99,7 +99,7 @@ pub(crate) fn builtin_attribute_shadowing(
name, name,
checker.source_type, checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist, &checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version, checker.target_version(),
) { ) {
// Ignore explicit overrides. // Ignore explicit overrides.
if class_def.decorator_list.iter().any(|decorator| { if class_def.decorator_list.iter().any(|decorator| {

View file

@ -61,7 +61,7 @@ pub(crate) fn builtin_import_shadowing(checker: &Checker, alias: &Alias) {
name.as_str(), name.as_str(),
checker.source_type, checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist, &checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version, checker.target_version(),
) { ) {
checker.report_diagnostic(Diagnostic::new( checker.report_diagnostic(Diagnostic::new(
BuiltinImportShadowing { BuiltinImportShadowing {

View file

@ -44,7 +44,7 @@ pub(crate) fn builtin_lambda_argument_shadowing(checker: &Checker, lambda: &Expr
name, name,
checker.source_type, checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist, &checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version, checker.target_version(),
) { ) {
checker.report_diagnostic(Diagnostic::new( checker.report_diagnostic(Diagnostic::new(
BuiltinLambdaArgumentShadowing { BuiltinLambdaArgumentShadowing {

View file

@ -63,7 +63,7 @@ pub(crate) fn builtin_variable_shadowing(checker: &Checker, name: &str, range: T
name, name,
checker.source_type, checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist, &checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version, checker.target_version(),
) { ) {
checker.report_diagnostic(Diagnostic::new( checker.report_diagnostic(Diagnostic::new(
BuiltinVariableShadowing { BuiltinVariableShadowing {

View file

@ -3,7 +3,7 @@ use std::path::{Component, Path, PathBuf};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata}; 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::path::is_module_file;
use ruff_python_stdlib::sys::is_known_standard_library; use ruff_python_stdlib::sys::is_known_standard_library;
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
@ -69,6 +69,7 @@ impl Violation for StdlibModuleShadowing {
pub(crate) fn stdlib_module_shadowing( pub(crate) fn stdlib_module_shadowing(
mut path: &Path, mut path: &Path,
settings: &LinterSettings, settings: &LinterSettings,
target_version: PythonVersion,
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {
if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file) { if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file) {
return None; return None;
@ -98,7 +99,7 @@ pub(crate) fn stdlib_module_shadowing(
let module_name = components.next()?; let module_name = components.next()?;
if is_allowed_module(settings, &module_name) { if is_allowed_module(settings, target_version, &module_name) {
return None; return None;
} }
@ -129,7 +130,7 @@ fn get_prefix<'a>(settings: &'a LinterSettings, path: &Path) -> Option<&'a PathB
prefix 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. // Shadowing private stdlib modules is okay.
// https://github.com/astral-sh/ruff/issues/12949 // https://github.com/astral-sh/ruff/issues/12949
if module.starts_with('_') && !module.starts_with("__") { if module.starts_with('_') && !module.starts_with("__") {
@ -145,5 +146,5 @@ fn is_allowed_module(settings: &LinterSettings, module: &str) -> bool {
return true; return true;
} }
!is_known_standard_library(settings.target_version.minor, module) !is_known_standard_library(version.minor, module)
} }

View file

@ -30,7 +30,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_future_annotations").join(path).as_path(), Path::new("flake8_future_annotations").join(path).as_path(),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY37, unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation) ..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation)
}, },
)?; )?;
@ -49,7 +49,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_future_annotations").join(path).as_path(), Path::new("flake8_future_annotations").join(path).as_path(),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY37, unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) ..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
}, },
)?; )?;

View file

@ -189,7 +189,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_pyi").join(path).as_path(), Path::new("flake8_pyi").join(path).as_path(),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY38, unresolved_target_version: PythonVersion::PY38,
..settings::LinterSettings::for_rule(rule_code) ..settings::LinterSettings::for_rule(rule_code)
}, },
)?; )?;

View file

@ -560,7 +560,7 @@ fn replace_custom_typevar_with_self(
/// This is because it was added to the `typing` module on Python 3.11, /// 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. /// but is available from the backport package `typing_extensions` on all versions.
fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String), ResolutionError> { 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" "typing"
} else { } else {
"typing_extensions" "typing_extensions"

View file

@ -67,7 +67,7 @@ pub(crate) fn no_return_argument_annotation(checker: &Checker, parameters: &ast:
if is_no_return(annotation, checker) { if is_no_return(annotation, checker) {
checker.report_diagnostic(Diagnostic::new( checker.report_diagnostic(Diagnostic::new(
NoReturnArgumentAnnotationInStub { NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= PythonVersion::PY311 { module: if checker.target_version() >= PythonVersion::PY311 {
TypingModule::Typing TypingModule::Typing
} else { } else {
TypingModule::TypingExtensions TypingModule::TypingExtensions

View file

@ -215,7 +215,7 @@ fn replace_with_self_fix(
let semantic = checker.semantic(); let semantic = checker.semantic();
let (self_import, self_binding) = { 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" "typing"
} else { } else {
"typing_extensions" "typing_extensions"

View file

@ -56,7 +56,7 @@ impl Violation for Pep484StylePositionalOnlyParameter {
/// PYI063 /// PYI063
pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast::StmtFunctionDef) { pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast::StmtFunctionDef) {
// PEP 570 was introduced in Python 3.8. // PEP 570 was introduced in Python 3.8.
if checker.settings.target_version < PythonVersion::PY38 { if checker.target_version() < PythonVersion::PY38 {
return; return;
} }

View file

@ -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() { let union_kind = if literal_elements.is_empty() {
UnionKind::NoUnion UnionKind::NoUnion
} else if (checker.settings.target_version >= PythonVersion::PY310) } else if (checker.target_version() >= PythonVersion::PY310) || checker.source_type.is_stub() {
|| checker.source_type.is_stub()
{
UnionKind::BitOr UnionKind::BitOr
} else { } else {
UnionKind::TypingOptional UnionKind::TypingOptional

View file

@ -667,7 +667,7 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar
return; return;
} }
let module = if checker.settings.target_version >= PythonVersion::PY310 { let module = if checker.target_version() >= PythonVersion::PY310 {
TypingModule::Typing TypingModule::Typing
} else { } else {
TypingModule::TypingExtensions TypingModule::TypingExtensions

View file

@ -3,7 +3,7 @@ use flake8_quotes::settings::Quote;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::visitor::{walk_f_string, Visitor}; 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 ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -61,7 +61,11 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
return; 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() { for part in string_like.parts() {
match part { match part {
@ -88,11 +92,15 @@ struct AvoidableEscapedQuoteChecker<'a> {
} }
impl<'a> 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 { Self {
locator, locator,
quotes_settings: &settings.flake8_quotes, quotes_settings: &settings.flake8_quotes,
supports_pep701: settings.target_version.supports_pep_701(), supports_pep701: target_version.supports_pep_701(),
diagnostics: vec![], diagnostics: vec![],
} }
} }

View file

@ -92,7 +92,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(), Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY39, unresolved_target_version: PythonVersion::PY39,
..settings::LinterSettings::for_rule(rule_code) ..settings::LinterSettings::for_rule(rule_code)
}, },
)?; )?;

View file

@ -10,7 +10,6 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rules::flake8_type_checking::helpers::quote_type_expression; use crate::rules::flake8_type_checking::helpers::quote_type_expression;
use crate::settings::LinterSettings;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
/// ## What it does /// ## What it does
@ -284,7 +283,7 @@ pub(crate) fn quoted_type_alias(
// explicit type aliases require some additional checks to avoid false positives // explicit type aliases require some additional checks to avoid false positives
if checker.semantic().in_annotated_type_alias_value() 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; return;
} }
@ -305,7 +304,7 @@ pub(crate) fn quoted_type_alias(
fn quotes_are_unremovable( fn quotes_are_unremovable(
semantic: &SemanticModel, semantic: &SemanticModel,
expr: &Expr, expr: &Expr,
settings: &LinterSettings, target_version: PythonVersion,
) -> bool { ) -> bool {
match expr { match expr {
Expr::BinOp(ast::ExprBinOp { Expr::BinOp(ast::ExprBinOp {
@ -313,11 +312,11 @@ fn quotes_are_unremovable(
}) => { }) => {
match op { match op {
Operator::BitOr => { Operator::BitOr => {
if settings.target_version < PythonVersion::PY310 { if target_version < PythonVersion::PY310 {
return true; return true;
} }
quotes_are_unremovable(semantic, left, settings) quotes_are_unremovable(semantic, left, target_version)
|| quotes_are_unremovable(semantic, right, settings) || quotes_are_unremovable(semantic, right, target_version)
} }
// for now we'll treat uses of other operators as unremovable quotes // for now we'll treat uses of other operators as unremovable quotes
// since that would make it an invalid type expression anyways. We skip // since that would make it an invalid type expression anyways. We skip
@ -330,7 +329,7 @@ fn quotes_are_unremovable(
value, value,
ctx: ExprContext::Load, ctx: ExprContext::Load,
.. ..
}) => quotes_are_unremovable(semantic, value, settings), }) => quotes_are_unremovable(semantic, value, target_version),
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
// for subscripts we don't know whether it's safe to do at runtime // 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. // 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() { if !semantic.in_type_checking_block() {
return true; return true;
} }
if quotes_are_unremovable(semantic, value, settings) { if quotes_are_unremovable(semantic, value, target_version) {
return true; return true;
} }
// for `typing.Annotated`, only analyze the first argument, since the rest may // 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 semantic.match_typing_qualified_name(&qualified_name, "Annotated") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
return !elts.is_empty() return !elts.is_empty()
&& quotes_are_unremovable(semantic, &elts[0], settings); && quotes_are_unremovable(semantic, &elts[0], target_version);
} }
return false; return false;
} }
} }
quotes_are_unremovable(semantic, slice, settings) quotes_are_unremovable(semantic, slice, target_version)
} }
Expr::Attribute(ast::ExprAttribute { value, .. }) => { Expr::Attribute(ast::ExprAttribute { value, .. }) => {
// for attributes we also don't know whether it's safe // for attributes we also don't know whether it's safe
if !semantic.in_type_checking_block() { if !semantic.in_type_checking_block() {
return true; 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, .. }) => { Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
for elt in elts { for elt in elts {
if quotes_are_unremovable(semantic, elt, settings) { if quotes_are_unremovable(semantic, elt, target_version) {
return true; return true;
} }
} }

View file

@ -307,7 +307,7 @@ pub(crate) fn typing_only_runtime_import(
checker.package(), checker.package(),
checker.settings.isort.detect_same_package, checker.settings.isort.detect_same_package,
&checker.settings.isort.known_modules, &checker.settings.isort.known_modules,
checker.settings.target_version, checker.target_version(),
checker.settings.isort.no_sections, checker.settings.isort.no_sections,
&checker.settings.isort.section_order, &checker.settings.isort.section_order,
&checker.settings.isort.default_section, &checker.settings.isort.default_section,

View file

@ -152,7 +152,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
), ),
// PTH115 // PTH115
// Python 3.9+ // Python 3.9+
["os", "readlink"] if checker.settings.target_version >= PythonVersion::PY39 => { ["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
Some(OsReadlink.into()) Some(OsReadlink.into())
} }
// PTH208, // PTH208,

View file

@ -3,7 +3,7 @@ use itertools::{EitherOrBoth, Itertools};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::whitespace::trailing_lines_end; 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_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::Tokens; use ruff_python_parser::Tokens;
@ -88,6 +88,7 @@ pub(crate) fn organize_imports(
package: Option<PackageRoot<'_>>, package: Option<PackageRoot<'_>>,
source_type: PySourceType, source_type: PySourceType,
tokens: &Tokens, tokens: &Tokens,
target_version: PythonVersion,
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {
let indentation = locator.slice(extract_indentation_range(&block.imports, locator)); let indentation = locator.slice(extract_indentation_range(&block.imports, locator));
let indentation = leading_indentation(indentation); let indentation = leading_indentation(indentation);
@ -127,7 +128,7 @@ pub(crate) fn organize_imports(
&settings.src, &settings.src,
package, package,
source_type, source_type,
settings.target_version, target_version,
&settings.isort, &settings.isort,
tokens, tokens,
); );

View file

@ -43,7 +43,7 @@ mod tests {
Path::new("perflint").join(path).as_path(), Path::new("perflint").join(path).as_path(),
&LinterSettings { &LinterSettings {
preview: PreviewMode::Enabled, preview: PreviewMode::Enabled,
target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::PY310,
..LinterSettings::for_rule(rule_code) ..LinterSettings::for_rule(rule_code)
}, },
)?; )?;

View file

@ -89,7 +89,7 @@ impl Violation for TryExceptInLoop {
/// PERF203 /// PERF203
pub(crate) fn try_except_in_loop(checker: &Checker, body: &[Stmt]) { 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; return;
} }

View file

@ -218,7 +218,7 @@ mod tests {
let diagnostics = test_snippet( let diagnostics = test_snippet(
"PythonFinalizationError", "PythonFinalizationError",
&LinterSettings { &LinterSettings {
target_version: ruff_python_ast::PythonVersion::PY312, unresolved_target_version: ruff_python_ast::PythonVersion::PY312,
..LinterSettings::for_rule(Rule::UndefinedName) ..LinterSettings::for_rule(Rule::UndefinedName)
}, },
); );

View file

@ -229,7 +229,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
checker.package(), checker.package(),
checker.settings.isort.detect_same_package, checker.settings.isort.detect_same_package,
&checker.settings.isort.known_modules, &checker.settings.isort.known_modules,
checker.settings.target_version, checker.target_version(),
checker.settings.isort.no_sections, checker.settings.isort.no_sections,
&checker.settings.isort.section_order, &checker.settings.isort.section_order,
&checker.settings.isort.default_section, &checker.settings.isort.default_section,

View file

@ -211,7 +211,7 @@ pub(crate) fn bad_str_strip_call(checker: &Checker, call: &ast::ExprCall) {
return; return;
} }
let removal = if checker.settings.target_version >= PythonVersion::PY39 { let removal = if checker.target_version() >= PythonVersion::PY39 {
RemovalKind::for_strip(strip) RemovalKind::for_strip(strip)
} else { } else {
None None

View file

@ -76,7 +76,7 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) {
} }
// If this is an allowed dunder method, abort. // 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; return;
} }

View file

@ -55,7 +55,7 @@ pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExp
return; 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()); let mut diagnostic = Diagnostic::new(UselessExceptionStatement, expr.range());
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
"raise ".to_string(), "raise ".to_string(),

View file

@ -156,7 +156,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("pyupgrade/UP041.py"), Path::new("pyupgrade/UP041.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias) ..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias)
}, },
)?; )?;
@ -169,7 +169,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("pyupgrade/UP040.py"), Path::new("pyupgrade/UP040.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY311, unresolved_target_version: PythonVersion::PY311,
..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) ..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias)
}, },
)?; )?;
@ -185,7 +185,7 @@ mod tests {
pyupgrade: pyupgrade::settings::Settings { pyupgrade: pyupgrade::settings::Settings {
keep_runtime_typing: true, keep_runtime_typing: true,
}, },
target_version: PythonVersion::PY37, unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
}, },
)?; )?;
@ -201,7 +201,7 @@ mod tests {
pyupgrade: pyupgrade::settings::Settings { pyupgrade: pyupgrade::settings::Settings {
keep_runtime_typing: true, keep_runtime_typing: true,
}, },
target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
}, },
)?; )?;
@ -214,7 +214,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"), Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY37, unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
}, },
)?; )?;
@ -227,7 +227,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"), Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
}, },
)?; )?;
@ -240,7 +240,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"), Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY37, unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rules([ ..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional, Rule::NonPEP604AnnotationOptional,
@ -256,7 +256,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"), Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rules([ ..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional, Rule::NonPEP604AnnotationOptional,
@ -272,7 +272,7 @@ mod tests {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("pyupgrade/UP017.py"), Path::new("pyupgrade/UP017.py"),
&settings::LinterSettings { &settings::LinterSettings {
target_version: PythonVersion::PY311, unresolved_target_version: PythonVersion::PY311,
..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC) ..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC)
}, },
)?; )?;
@ -286,7 +286,7 @@ mod tests {
Path::new("pyupgrade/UP044.py"), Path::new("pyupgrade/UP044.py"),
&settings::LinterSettings { &settings::LinterSettings {
preview: PreviewMode::Enabled, preview: PreviewMode::Enabled,
target_version: PythonVersion::PY311, unresolved_target_version: PythonVersion::PY311,
..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack) ..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack)
}, },
)?; )?;

View file

@ -722,7 +722,7 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
checker.locator(), checker.locator(),
checker.stylist(), checker.stylist(),
checker.tokens(), checker.tokens(),
checker.settings.target_version, checker.target_version(),
); );
for (operation, fix) in fixer.without_renames() { for (operation, fix) in fixer.without_renames() {

View file

@ -115,7 +115,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) {
let Some(version) = extract_version(elts) else { let Some(version) = extract_version(elts) else {
return; return;
}; };
let target = checker.settings.target_version; let target = checker.target_version();
match version_always_less_than( match version_always_less_than(
&version, &version,
target, target,

View file

@ -106,7 +106,7 @@ impl Violation for NonPEP695GenericClass {
/// UP046 /// UP046
pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassDef) { pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassDef) {
// PEP-695 syntax is only available on Python 3.12+ // PEP-695 syntax is only available on Python 3.12+
if checker.settings.target_version < PythonVersion::PY312 { if checker.target_version() < PythonVersion::PY312 {
return; return;
} }

View file

@ -98,7 +98,7 @@ impl Violation for NonPEP695GenericFunction {
/// UP047 /// UP047
pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &StmtFunctionDef) { pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &StmtFunctionDef) {
// PEP-695 syntax is only available on Python 3.12+ // PEP-695 syntax is only available on Python 3.12+
if checker.settings.target_version < PythonVersion::PY312 { if checker.target_version() < PythonVersion::PY312 {
return; return;
} }

View file

@ -111,7 +111,7 @@ impl Violation for NonPEP695TypeAlias {
/// UP040 /// UP040
pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) { 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; return;
} }
@ -182,7 +182,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) {
/// UP040 /// UP040
pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) { 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; return;
} }

View file

@ -162,7 +162,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
}; };
match expr.as_ref() { match expr.as_ref() {
Expr::Name(_) | Expr::Attribute(_) => { 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); 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`. // List of aliases to replace with `TimeoutError`.
let mut aliases: Vec<&Expr> = vec![]; let mut aliases: Vec<&Expr> = vec![];
for element in tuple { 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); aliases.push(element);
} }
} }
@ -185,7 +185,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
/// UP041 /// UP041
pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) { 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); atom_diagnostic(checker, func);
} }
} }
@ -193,7 +193,7 @@ pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) {
/// UP041 /// UP041
pub(crate) fn timeout_error_alias_raise(checker: &Checker, expr: &Expr) { pub(crate) fn timeout_error_alias_raise(checker: &Checker, expr: &Expr) {
if matches!(expr, Expr::Name(_) | Expr::Attribute(_)) { 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); atom_diagnostic(checker, expr);
} }
} }

View file

@ -98,7 +98,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement:
checker.semantic(), checker.semantic(),
)?; )?;
let binding_edit = Edit::range_replacement(binding, expr.range()); 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 Applicability::Safe
} else { } else {
Applicability::Unsafe Applicability::Unsafe
@ -122,7 +122,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement:
Ok(Fix::applicable_edits( Ok(Fix::applicable_edits(
import_edit, import_edit,
[reference_edit], [reference_edit],
if checker.settings.target_version >= PythonVersion::PY310 { if checker.target_version() >= PythonVersion::PY310 {
Applicability::Safe Applicability::Safe
} else { } else {
Applicability::Unsafe Applicability::Unsafe

View file

@ -142,7 +142,7 @@ pub(crate) fn non_pep604_annotation(
&& !checker.semantic().in_complex_string_type_definition() && !checker.semantic().in_complex_string_type_definition()
&& is_allowed_value(slice); && is_allowed_value(slice);
let applicability = if checker.settings.target_version >= PythonVersion::PY310 { let applicability = if checker.target_version() >= PythonVersion::PY310 {
Applicability::Safe Applicability::Safe
} else { } else {
Applicability::Unsafe Applicability::Unsafe

View file

@ -53,7 +53,7 @@ impl Violation for NonPEP646Unpack {
/// UP044 /// UP044
pub(crate) fn use_pep646_unpack(checker: &Checker, expr: &ExprSubscript) { pub(crate) fn use_pep646_unpack(checker: &Checker, expr: &ExprSubscript) {
if checker.settings.target_version < PythonVersion::PY311 { if checker.target_version() < PythonVersion::PY311 {
return; return;
} }

View file

@ -59,7 +59,7 @@ impl AlwaysFixableViolation for BitCount {
/// FURB161 /// FURB161
pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) {
// `int.bit_count()` was added in Python 3.10 // `int.bit_count()` was added in Python 3.10
if checker.settings.target_version < PythonVersion::PY310 { if checker.target_version() < PythonVersion::PY310 {
return; return;
} }

View file

@ -82,7 +82,7 @@ impl AlwaysFixableViolation for FromisoformatReplaceZ {
/// FURB162 /// FURB162
pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) { pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) {
if checker.settings.target_version < PythonVersion::PY311 { if checker.target_version() < PythonVersion::PY311 {
return; return;
} }

View file

@ -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. // First we go through all the items in the statement and find all `open` operations.
let candidates = find_file_opens( let candidates = find_file_opens(with, checker.semantic(), true, checker.target_version());
with,
checker.semantic(),
true,
checker.settings.target_version,
);
if candidates.is_empty() { if candidates.is_empty() {
return; return;
} }

View file

@ -69,7 +69,7 @@ impl AlwaysFixableViolation for SliceToRemovePrefixOrSuffix {
/// FURB188 /// FURB188
pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprIf) { 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; return;
} }
@ -100,7 +100,7 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI
/// FURB188 /// FURB188
pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtIf) { 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; return;
} }
if let Some(removal_data) = affix_removal_data_stmt(if_stmt) { if let Some(removal_data) = affix_removal_data_stmt(if_stmt) {

View file

@ -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. // First we go through all the items in the statement and find all `open` operations.
let candidates = find_file_opens( let candidates = find_file_opens(with, checker.semantic(), false, checker.target_version());
with,
checker.semantic(),
false,
checker.settings.target_version,
);
if candidates.is_empty() { if candidates.is_empty() {
return; return;
} }

View file

@ -129,7 +129,7 @@ mod tests {
extend_markup_names: vec![], extend_markup_names: vec![],
allowed_markup_calls: vec![], allowed_markup_calls: vec![],
}, },
target_version: PythonVersion::PY310, unresolved_target_version: PythonVersion::PY310,
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript) ..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
}, },
)?; )?;

View file

@ -79,7 +79,7 @@ impl Violation for ClassWithMixedTypeVars {
/// RUF053 /// RUF053
pub(crate) fn class_with_mixed_type_vars(checker: &Checker, class_def: &StmtClassDef) { 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; return;
} }

View file

@ -177,11 +177,11 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
let Some(expr) = type_hint_explicitly_allows_none( let Some(expr) = type_hint_explicitly_allows_none(
parsed_annotation.expression(), parsed_annotation.expression(),
checker, checker,
checker.settings.target_version, checker.target_version(),
) else { ) else {
continue; continue;
}; };
let conversion_type = checker.settings.target_version.into(); let conversion_type = checker.target_version().into();
let mut diagnostic = let mut diagnostic =
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());
@ -192,14 +192,12 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
} }
} else { } else {
// Unquoted annotation. // Unquoted annotation.
let Some(expr) = type_hint_explicitly_allows_none( let Some(expr) =
annotation, type_hint_explicitly_allows_none(annotation, checker, checker.target_version())
checker, else {
checker.settings.target_version,
) else {
continue; continue;
}; };
let conversion_type = checker.settings.target_version.into(); let conversion_type = checker.target_version().into();
let mut diagnostic = let mut diagnostic =
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());

View file

@ -88,7 +88,7 @@ pub(crate) fn subscript_with_parenthesized_tuple(checker: &Checker, subscript: &
// to a syntax error in Python 3.10. // to a syntax error in Python 3.10.
// This is no longer a syntax error starting in Python 3.11 // 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 // 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 && !prefer_parentheses
&& tuple_subscript.iter().any(Expr::is_starred_expr) && tuple_subscript.iter().any(Expr::is_starred_expr)
{ {

View file

@ -8,6 +8,7 @@ use rustc_hash::FxHashSet;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::LazyLock; use std::sync::LazyLock;
use types::CompiledPerFileTargetVersionList;
use crate::codes::RuleCodePrefix; use crate::codes::RuleCodePrefix;
use ruff_macros::CacheKey; use ruff_macros::CacheKey;
@ -219,7 +220,21 @@ pub struct LinterSettings {
pub per_file_ignores: CompiledPerFileIgnoreList, pub per_file_ignores: CompiledPerFileIgnoreList,
pub fix_safety: FixSafetyTable, 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 preview: PreviewMode,
pub explicit_preview_rules: bool, pub explicit_preview_rules: bool,
@ -281,7 +296,8 @@ impl Display for LinterSettings {
self.per_file_ignores, self.per_file_ignores,
self.fix_safety | nested, self.fix_safety | nested,
self.target_version, self.unresolved_target_version,
self.per_file_target_version,
self.preview, self.preview,
self.explicit_preview_rules, self.explicit_preview_rules,
self.extension | debug, self.extension | debug,
@ -361,7 +377,7 @@ impl LinterSettings {
pub fn for_rule(rule_code: Rule) -> Self { pub fn for_rule(rule_code: Rule) -> Self {
Self { Self {
rules: RuleTable::from_iter([rule_code]), rules: RuleTable::from_iter([rule_code]),
target_version: PythonVersion::latest(), unresolved_target_version: PythonVersion::latest(),
..Self::default() ..Self::default()
} }
} }
@ -369,7 +385,7 @@ impl LinterSettings {
pub fn for_rules(rules: impl IntoIterator<Item = Rule>) -> Self { pub fn for_rules(rules: impl IntoIterator<Item = Rule>) -> Self {
Self { Self {
rules: RuleTable::from_iter(rules), rules: RuleTable::from_iter(rules),
target_version: PythonVersion::latest(), unresolved_target_version: PythonVersion::latest(),
..Self::default() ..Self::default()
} }
} }
@ -377,7 +393,8 @@ impl LinterSettings {
pub fn new(project_root: &Path) -> Self { pub fn new(project_root: &Path) -> Self {
Self { Self {
exclude: FilePatternSet::default(), 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(), project_root: project_root.to_path_buf(),
rules: DEFAULT_SELECTORS rules: DEFAULT_SELECTORS
.iter() .iter()
@ -439,9 +456,20 @@ impl LinterSettings {
#[must_use] #[must_use]
pub fn with_target_version(mut self, target_version: PythonVersion) -> Self { pub fn with_target_version(mut self, target_version: PythonVersion) -> Self {
self.target_version = target_version; self.unresolved_target_version = target_version;
self 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 { impl Default for LinterSettings {

View file

@ -5,8 +5,9 @@ use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use std::string::ToString; use std::string::ToString;
use anyhow::{bail, Result}; use anyhow::{bail, Context, Result};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use log::debug;
use pep440_rs::{VersionSpecifier, VersionSpecifiers}; use pep440_rs::{VersionSpecifier, VersionSpecifiers};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{de, Deserialize, Deserializer, Serialize}; 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)] #[derive(Debug, Clone)]
pub struct PerFileIgnore { pub struct PerFile<T> {
pub(crate) basename: String, /// The glob pattern used to construct the [`PerFile`].
pub(crate) absolute: PathBuf, basename: String,
pub(crate) negated: bool, /// The same pattern as `basename` but normalized to the project root directory.
pub(crate) rules: RuleSet, 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 { impl<T> PerFile<T> {
pub fn new( /// Construct a new [`PerFile`] from the given glob `pattern` and containing `data`.
mut pattern: String, ///
prefixes: &[RuleSelector], /// If provided, `project_root` is used to construct a second glob pattern normalized to the
project_root: Option<&Path>, /// project root directory. See [`fs::normalize_path_to`] for more details.
) -> Self { fn new(mut pattern: String, project_root: Option<&Path>, data: T) -> 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();
let negated = pattern.starts_with('!'); let negated = pattern.starts_with('!');
if negated { if negated {
pattern.drain(..1); pattern.drain(..1);
@ -304,11 +308,26 @@ impl PerFileIgnore {
basename: pattern, basename: pattern,
absolute, absolute,
negated, negated,
rules, data,
} }
} }
} }
/// Per-file ignored linting rules.
///
/// See [`PerFile`] for details of the representation.
#[derive(Debug, Clone)]
pub struct PerFileIgnore(PerFile<RuleSet>);
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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct PatternPrefixPair { pub struct PatternPrefixPair {
pub pattern: String, pub pattern: String,
@ -558,15 +577,47 @@ impl Display for RequiredVersion {
/// pattern matching. /// pattern matching.
pub type IdentifierPattern = glob::Pattern; pub type IdentifierPattern = glob::Pattern;
#[derive(Debug, Clone, CacheKey)] /// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage.
pub struct CompiledPerFileIgnore { #[derive(Debug, Clone)]
pub struct CompiledPerFile<T> {
pub absolute_matcher: GlobMatcher, pub absolute_matcher: GlobMatcher,
pub basename_matcher: GlobMatcher, pub basename_matcher: GlobMatcher,
pub negated: bool, pub negated: bool,
pub rules: RuleSet, pub data: T,
} }
impl Display for CompiledPerFileIgnore { impl<T> CompiledPerFile<T> {
fn new(
absolute_matcher: GlobMatcher,
basename_matcher: GlobMatcher,
negated: bool,
data: T,
) -> Self {
Self {
absolute_matcher,
basename_matcher,
negated,
data,
}
}
}
impl<T> CacheKey for CompiledPerFile<T>
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<T> Display for CompiledPerFile<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! { display_settings! {
formatter = f, formatter = f,
@ -574,52 +625,130 @@ impl Display for CompiledPerFileIgnore {
self.absolute_matcher | globmatcher, self.absolute_matcher | globmatcher,
self.basename_matcher | globmatcher, self.basename_matcher | globmatcher,
self.negated, self.negated,
self.rules, self.data,
] ]
} }
Ok(()) Ok(())
} }
} }
#[derive(Debug, Clone, CacheKey, Default)] /// A sequence of [`CompiledPerFile<T>`].
pub struct CompiledPerFileIgnoreList { #[derive(Debug, Clone, Default)]
// Ordered as (absolute path matcher, basename matcher, rules) pub struct CompiledPerFileList<T> {
ignores: Vec<CompiledPerFileIgnore>, inner: Vec<CompiledPerFile<T>>,
} }
impl CompiledPerFileIgnoreList { impl<T> CacheKey for CompiledPerFileList<T>
/// Given a list of patterns, create a `GlobSet`. where
pub fn resolve(per_file_ignores: Vec<PerFileIgnore>) -> Result<Self> { T: CacheKey,
let ignores: Result<Vec<_>> = per_file_ignores {
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.inner.cache_key(state);
}
}
impl<T> CompiledPerFileList<T> {
/// 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<Item = PerFile<T>>) -> Result<Self> {
let inner: Result<Vec<_>> = per_file_items
.into_iter() .into_iter()
.map(|per_file_ignore| { .map(|per_file_ignore| {
// Construct absolute path matcher. // Construct absolute path matcher.
let absolute_matcher = let absolute_matcher = Glob::new(&per_file_ignore.absolute.to_string_lossy())
Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher(); .with_context(|| format!("invalid glob {:?}", per_file_ignore.absolute))?
.compile_matcher();
// Construct basename matcher. // Construct basename matcher.
let basename_matcher = Glob::new(&per_file_ignore.basename)?.compile_matcher(); let basename_matcher = Glob::new(&per_file_ignore.basename)
.with_context(|| format!("invalid glob {:?}", per_file_ignore.basename))?
.compile_matcher();
Ok(CompiledPerFileIgnore { Ok(CompiledPerFile::new(
absolute_matcher, absolute_matcher,
basename_matcher, basename_matcher,
negated: per_file_ignore.negated, per_file_ignore.negated,
rules: per_file_ignore.rules, per_file_ignore.data,
}) ))
}) })
.collect(); .collect();
Ok(Self { ignores: ignores? }) Ok(Self { inner: inner? })
}
pub(crate) fn is_empty(&self) -> bool {
self.inner.is_empty()
} }
} }
impl Display for CompiledPerFileIgnoreList { impl<T: std::fmt::Debug> CompiledPerFileList<T> {
/// 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<Item = &'p T>
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<T> Display for CompiledPerFileList<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.ignores.is_empty() { if self.inner.is_empty() {
write!(f, "{{}}")?; write!(f, "{{}}")?;
} else { } else {
writeln!(f, "{{")?; writeln!(f, "{{")?;
for ignore in &self.ignores { for value in &self.inner {
writeln!(f, "\t{ignore}")?; writeln!(f, "\t{value}")?;
} }
write!(f, "}}")?; write!(f, "}}")?;
} }
@ -627,11 +756,70 @@ impl Display for CompiledPerFileIgnoreList {
} }
} }
#[derive(Debug, Clone, CacheKey, Default)]
pub struct CompiledPerFileIgnoreList(CompiledPerFileList<RuleSet>);
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<PerFileIgnore>) -> Result<Self> {
Ok(Self(CompiledPerFileList::resolve(
per_file_ignores.into_iter().map(|ignore| ignore.0),
)?))
}
}
impl Deref for CompiledPerFileIgnoreList { impl Deref for CompiledPerFileIgnoreList {
type Target = Vec<CompiledPerFileIgnore>; type Target = CompiledPerFileList<RuleSet>;
fn deref(&self) -> &Self::Target { 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<ast::PythonVersion>);
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<ast::PythonVersion>);
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<PerFileTargetVersion>) -> Result<Self> {
Ok(Self(CompiledPerFileList::resolve(
per_file_versions.into_iter().map(|version| version.0),
)?))
}
pub fn is_match(&self, path: &Path) -> Option<ast::PythonVersion> {
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)
} }
} }

View file

@ -1,3 +1,5 @@
use std::path::Path;
use ruff_formatter::PrintedRange; use ruff_formatter::PrintedRange;
use ruff_python_ast::PySourceType; use ruff_python_ast::PySourceType;
use ruff_python_formatter::{format_module_source, FormatModuleError}; use ruff_python_formatter::{format_module_source, FormatModuleError};
@ -10,8 +12,10 @@ pub(crate) fn format(
document: &TextDocument, document: &TextDocument,
source_type: PySourceType, source_type: PySourceType,
formatter_settings: &FormatterSettings, formatter_settings: &FormatterSettings,
path: Option<&Path>,
) -> crate::Result<Option<String>> { ) -> crate::Result<Option<String>> {
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) { match format_module_source(document.contents(), format_options) {
Ok(formatted) => { Ok(formatted) => {
let formatted = formatted.into_code(); let formatted = formatted.into_code();
@ -36,8 +40,10 @@ pub(crate) fn format_range(
source_type: PySourceType, source_type: PySourceType,
formatter_settings: &FormatterSettings, formatter_settings: &FormatterSettings,
range: TextRange, range: TextRange,
path: Option<&Path>,
) -> crate::Result<Option<PrintedRange>> { ) -> crate::Result<Option<PrintedRange>> {
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) { match ruff_python_formatter::format_range(document.contents(), range, format_options) {
Ok(formatted) => { Ok(formatted) => {
@ -56,3 +62,146 @@ pub(crate) fn format_range(
Err(err) => Err(err.into()), 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(())
}
}

View file

@ -85,9 +85,10 @@ fn format_text_document(
let settings = query.settings(); let settings = query.settings();
// If the document is excluded, return early. // 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( if is_document_excluded_for_formatting(
&file_path, file_path,
&settings.file_resolver, &settings.file_resolver,
&settings.formatter, &settings.formatter,
text_document.language_id(), text_document.language_id(),
@ -97,7 +98,12 @@ fn format_text_document(
} }
let source = text_document.contents(); let source = text_document.contents();
let formatted = crate::format::format(text_document, query.source_type(), &settings.formatter) let formatted = crate::format::format(
text_document,
query.source_type(),
&settings.formatter,
file_path.as_deref(),
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?; .with_failure_code(lsp_server::ErrorCode::InternalError)?;
let Some(mut formatted) = formatted else { let Some(mut formatted) = formatted else {
return Ok(None); return Ok(None);

View file

@ -49,9 +49,10 @@ fn format_text_document_range(
let settings = query.settings(); let settings = query.settings();
// If the document is excluded, return early. // 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( if is_document_excluded_for_formatting(
&file_path, file_path,
&settings.file_resolver, &settings.file_resolver,
&settings.formatter, &settings.formatter,
text_document.language_id(), text_document.language_id(),
@ -68,6 +69,7 @@ fn format_text_document_range(
query.source_type(), query.source_type(),
&settings.formatter, &settings.formatter,
range, range,
file_path.as_deref(),
) )
.with_failure_code(lsp_server::ErrorCode::InternalError)?; .with_failure_code(lsp_server::ErrorCode::InternalError)?;

View file

@ -303,7 +303,7 @@ impl<'a> ParsedModule<'a> {
// TODO(konstin): Add an options for py/pyi to the UI (2/2) // TODO(konstin): Add an options for py/pyi to the UI (2/2)
let options = settings let options = settings
.formatter .formatter
.to_format_options(PySourceType::default(), self.source_code) .to_format_options(PySourceType::default(), self.source_code, None)
.with_source_map_generation(SourceMapGeneration::Enabled); .with_source_map_generation(SourceMapGeneration::Enabled);
format_module_ast( format_module_ast(

View file

@ -9,7 +9,7 @@ use std::num::{NonZeroU16, NonZeroU8};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Context, Result};
use glob::{glob, GlobError, Paths, PatternError}; use glob::{glob, GlobError, Paths, PatternError};
use itertools::Itertools; use itertools::Itertools;
use regex::Regex; 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::fix_safety_table::FixSafetyTable;
use ruff_linter::settings::rule_table::RuleTable; use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::{ use ruff_linter::settings::types::{
CompiledPerFileIgnoreList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, CompiledPerFileIgnoreList, CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern,
PerFileIgnore, PreviewMode, RequiredVersion, UnsafeFixes, FilePatternSet, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode,
RequiredVersion, UnsafeFixes,
}; };
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS}; use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
use ruff_linter::{ use ruff_linter::{
@ -138,6 +139,7 @@ pub struct Configuration {
pub namespace_packages: Option<Vec<PathBuf>>, pub namespace_packages: Option<Vec<PathBuf>>,
pub src: Option<Vec<PathBuf>>, pub src: Option<Vec<PathBuf>>,
pub target_version: Option<ast::PythonVersion>, pub target_version: Option<ast::PythonVersion>,
pub per_file_target_version: Option<Vec<PerFileTargetVersion>>,
// Global formatting options // Global formatting options
pub line_length: Option<LineLength>, pub line_length: Option<LineLength>,
@ -174,11 +176,17 @@ impl Configuration {
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled, 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 { let formatter = FormatterSettings {
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
extension: self.extension.clone().unwrap_or_default(), extension: self.extension.clone().unwrap_or_default(),
preview: format_preview, preview: format_preview,
target_version, unresolved_target_version: target_version,
per_file_target_version: per_file_target_version.clone(),
line_width: self line_width: self
.line_length .line_length
.map_or(format_defaults.line_width, |length| { .map_or(format_defaults.line_width, |length| {
@ -278,7 +286,8 @@ impl Configuration {
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?, exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
extension: self.extension.unwrap_or_default(), extension: self.extension.unwrap_or_default(),
preview: lint_preview, preview: lint_preview,
target_version, unresolved_target_version: target_version,
per_file_target_version,
project_root: project_root.to_path_buf(), project_root: project_root.to_path_buf(),
allowed_confusables: lint allowed_confusables: lint
.allowed_confusables .allowed_confusables
@ -533,6 +542,18 @@ impl Configuration {
.map(|src| resolve_src(&src, project_root)) .map(|src| resolve_src(&src, project_root))
.transpose()?, .transpose()?,
target_version: options.target_version.map(ast::PythonVersion::from), 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 // `--extension` is a hidden command-line argument that isn't supported in configuration
// files at present. // files at present.
extension: None, extension: None,
@ -580,6 +601,9 @@ impl Configuration {
show_fixes: self.show_fixes.or(config.show_fixes), show_fixes: self.show_fixes.or(config.show_fixes),
src: self.src.or(config.src), src: self.src.or(config.src),
target_version: self.target_version.or(config.target_version), 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), preview: self.preview.or(config.preview),
extension: self.extension.or(config.extension), extension: self.extension.or(config.extension),

View file

@ -333,6 +333,29 @@ pub struct Options {
)] )]
pub target_version: Option<PythonVersion>, pub target_version: Option<PythonVersion>,
/// 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<FxHashMap<String, PythonVersion>>,
/// The directories to consider when resolving first- vs. third-party /// The directories to consider when resolving first- vs. third-party
/// imports. /// imports.
/// ///

View file

@ -4,11 +4,12 @@ use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_graph::AnalyzeSettings; use ruff_graph::AnalyzeSettings;
use ruff_linter::display_settings; use ruff_linter::display_settings;
use ruff_linter::settings::types::{ use ruff_linter::settings::types::{
ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, UnsafeFixes, CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat,
UnsafeFixes,
}; };
use ruff_linter::settings::LinterSettings; use ruff_linter::settings::LinterSettings;
use ruff_macros::CacheKey; use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType; use ruff_python_ast::{PySourceType, PythonVersion};
use ruff_python_formatter::{ use ruff_python_formatter::{
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions, DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions,
QuoteStyle, QuoteStyle,
@ -164,7 +165,17 @@ pub struct FormatterSettings {
pub exclude: FilePatternSet, pub exclude: FilePatternSet,
pub extension: ExtensionMapping, pub extension: ExtensionMapping,
pub preview: PreviewMode, 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, pub line_width: LineWidth,
@ -182,7 +193,16 @@ pub struct FormatterSettings {
} }
impl 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 { let line_ending = match self.line_ending {
LineEnding::Lf => ruff_formatter::printer::LineEnding::LineFeed, LineEnding::Lf => ruff_formatter::printer::LineEnding::LineFeed,
LineEnding::CrLf => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed, LineEnding::CrLf => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
@ -205,7 +225,7 @@ impl FormatterSettings {
}; };
PyFormatOptions::from_source_type(source_type) 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_style(self.indent_style)
.with_indent_width(self.indent_width) .with_indent_width(self.indent_width)
.with_quote_style(self.quote_style) .with_quote_style(self.quote_style)
@ -216,6 +236,17 @@ impl FormatterSettings {
.with_docstring_code(self.docstring_code_format) .with_docstring_code(self.docstring_code_format)
.with_docstring_code_line_width(self.docstring_code_line_width) .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 { impl Default for FormatterSettings {
@ -225,7 +256,8 @@ impl Default for FormatterSettings {
Self { Self {
exclude: FilePatternSet::default(), exclude: FilePatternSet::default(),
extension: ExtensionMapping::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, preview: PreviewMode::Disabled,
line_width: default_options.line_width(), line_width: default_options.line_width(),
line_ending: LineEnding::Auto, line_ending: LineEnding::Auto,
@ -247,7 +279,8 @@ impl fmt::Display for FormatterSettings {
namespace = "formatter", namespace = "formatter",
fields = [ fields = [
self.exclude, self.exclude,
self.target_version, self.unresolved_target_version,
self.per_file_target_version,
self.preview, self.preview,
self.line_width, self.line_width,
self.line_ending, self.line_ending,

10
ruff.schema.json generated
View file

@ -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": { "preview": {
"description": "Whether to enable preview mode. When preview mode is enabled, Ruff will use unstable rules, fixes, and formatting.", "description": "Whether to enable preview mode. When preview mode is enabled, Ruff will use unstable rules, fixes, and formatting.",
"type": [ "type": [