mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
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:
parent
42a5f5ef6a
commit
e7a6c19e3a
78 changed files with 820 additions and 274 deletions
|
@ -341,7 +341,7 @@ pub(crate) fn format_source(
|
|||
) -> Result<FormattedSource, FormatCommandError> {
|
||||
match &source_kind {
|
||||
SourceKind::Python(unformatted) => {
|
||||
let options = settings.to_format_options(source_type, unformatted);
|
||||
let options = settings.to_format_options(source_type, unformatted, path);
|
||||
|
||||
let formatted = if let Some(range) = range {
|
||||
let line_index = LineIndex::from_source_text(unformatted);
|
||||
|
@ -391,7 +391,7 @@ pub(crate) fn format_source(
|
|||
));
|
||||
}
|
||||
|
||||
let options = settings.to_format_options(source_type, notebook.source_code());
|
||||
let options = settings.to_format_options(source_type, notebook.source_code(), path);
|
||||
|
||||
let mut output: Option<String> = None;
|
||||
let mut last: Option<TextSize> = None;
|
||||
|
|
|
@ -2086,3 +2086,50 @@ fn range_formatting_notebook() {
|
|||
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks.
|
||||
");
|
||||
}
|
||||
|
||||
/// Test that the formatter respects `per-file-target-version`. Context managers can't be
|
||||
/// parenthesized like this before Python 3.10.
|
||||
///
|
||||
/// Adapted from <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 -----
|
||||
"#);
|
||||
}
|
||||
|
|
|
@ -2567,3 +2567,63 @@ fn a005_module_shadowing_strict_default() -> Result<()> {
|
|||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that the linter respects per-file-target-version.
|
||||
#[test]
|
||||
fn per_file_target_version_linter() {
|
||||
// without per-file-target-version, there should be one UP046 error
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--target-version", "py312"])
|
||||
.args(["--select", "UP046"]) // only triggers on 3.12+
|
||||
.args(["--stdin-filename", "test.py"])
|
||||
.arg("--preview")
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class A(Generic[T]):
|
||||
var: T
|
||||
"#),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters
|
||||
Found 1 error.
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
// with per-file-target-version, there should be no errors because the new generic syntax is
|
||||
// unavailable
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--target-version", "py312"])
|
||||
.args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#])
|
||||
.args(["--select", "UP046"]) // only triggers on 3.12+
|
||||
.args(["--stdin-filename", "test.py"])
|
||||
.arg("--preview")
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class A(Generic[T]):
|
||||
var: T
|
||||
"#),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -189,7 +189,8 @@ linter.rules.should_fix = [
|
|||
linter.per_file_ignores = {}
|
||||
linter.safety_table.forced_safe = []
|
||||
linter.safety_table.forced_unsafe = []
|
||||
linter.target_version = 3.7
|
||||
linter.unresolved_target_version = 3.7
|
||||
linter.per_file_target_version = {}
|
||||
linter.preview = disabled
|
||||
linter.explicit_preview_rules = false
|
||||
linter.extension = ExtensionMapping({})
|
||||
|
@ -373,7 +374,8 @@ linter.ruff.allowed_markup_calls = []
|
|||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
formatter.target_version = 3.7
|
||||
formatter.unresolved_target_version = 3.7
|
||||
formatter.per_file_target_version = {}
|
||||
formatter.preview = disabled
|
||||
formatter.line_width = 100
|
||||
formatter.line_ending = auto
|
||||
|
|
|
@ -34,8 +34,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
{
|
||||
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::PY310
|
||||
&& checker.settings.target_version >= PythonVersion::PY37
|
||||
&& checker.target_version() < PythonVersion::PY310
|
||||
&& checker.target_version() >= PythonVersion::PY37
|
||||
&& checker.semantic.in_annotation()
|
||||
&& !checker.settings.pyupgrade.keep_runtime_typing
|
||||
{
|
||||
|
@ -49,8 +49,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
Rule::NonPEP604AnnotationOptional,
|
||||
]) {
|
||||
if checker.source_type.is_stub()
|
||||
|| checker.settings.target_version >= PythonVersion::PY310
|
||||
|| (checker.settings.target_version >= PythonVersion::PY37
|
||||
|| checker.target_version() >= PythonVersion::PY310
|
||||
|| (checker.target_version() >= PythonVersion::PY37
|
||||
&& checker.semantic.future_annotations_or_stub()
|
||||
&& checker.semantic.in_annotation()
|
||||
&& !checker.settings.pyupgrade.keep_runtime_typing)
|
||||
|
@ -64,7 +64,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
// Ex) list[...]
|
||||
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::PY39
|
||||
&& checker.target_version() < PythonVersion::PY39
|
||||
&& checker.semantic.in_annotation()
|
||||
&& checker.semantic.in_runtime_evaluated_annotation()
|
||||
&& !checker.semantic.in_string_type_definition()
|
||||
|
@ -135,7 +135,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
}
|
||||
|
||||
if checker.enabled(Rule::UnnecessaryDefaultTypeArgs) {
|
||||
if checker.settings.target_version >= PythonVersion::PY313 {
|
||||
if checker.target_version() >= PythonVersion::PY313 {
|
||||
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
|
||||
}
|
||||
}
|
||||
|
@ -268,8 +268,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
{
|
||||
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::PY39
|
||||
&& checker.settings.target_version >= PythonVersion::PY37
|
||||
&& checker.target_version() < PythonVersion::PY39
|
||||
&& checker.target_version() >= PythonVersion::PY37
|
||||
&& checker.semantic.in_annotation()
|
||||
&& !checker.settings.pyupgrade.keep_runtime_typing
|
||||
{
|
||||
|
@ -278,8 +278,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
}
|
||||
if checker.enabled(Rule::NonPEP585Annotation) {
|
||||
if checker.source_type.is_stub()
|
||||
|| checker.settings.target_version >= PythonVersion::PY39
|
||||
|| (checker.settings.target_version >= PythonVersion::PY37
|
||||
|| checker.target_version() >= PythonVersion::PY39
|
||||
|| (checker.target_version() >= PythonVersion::PY37
|
||||
&& checker.semantic.future_annotations_or_stub()
|
||||
&& checker.semantic.in_annotation()
|
||||
&& !checker.settings.pyupgrade.keep_runtime_typing)
|
||||
|
@ -378,8 +378,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
|
||||
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::PY39
|
||||
&& checker.settings.target_version >= PythonVersion::PY37
|
||||
&& checker.target_version() < PythonVersion::PY39
|
||||
&& checker.target_version() >= PythonVersion::PY37
|
||||
&& checker.semantic.in_annotation()
|
||||
&& !checker.settings.pyupgrade.keep_runtime_typing
|
||||
{
|
||||
|
@ -390,8 +390,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
}
|
||||
if checker.enabled(Rule::NonPEP585Annotation) {
|
||||
if checker.source_type.is_stub()
|
||||
|| checker.settings.target_version >= PythonVersion::PY39
|
||||
|| (checker.settings.target_version >= PythonVersion::PY37
|
||||
|| checker.target_version() >= PythonVersion::PY39
|
||||
|| (checker.target_version() >= PythonVersion::PY37
|
||||
&& checker.semantic.future_annotations_or_stub()
|
||||
&& checker.semantic.in_annotation()
|
||||
&& !checker.settings.pyupgrade.keep_runtime_typing)
|
||||
|
@ -405,7 +405,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
refurb::rules::regex_flag_alias(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::DatetimeTimezoneUTC) {
|
||||
if checker.settings.target_version >= PythonVersion::PY311 {
|
||||
if checker.target_version() >= PythonVersion::PY311 {
|
||||
pyupgrade::rules::datetime_utc_alias(checker, expr);
|
||||
}
|
||||
}
|
||||
|
@ -610,12 +610,12 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
pyupgrade::rules::os_error_alias_call(checker, func);
|
||||
}
|
||||
if checker.enabled(Rule::TimeoutErrorAlias) {
|
||||
if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
if checker.target_version() >= PythonVersion::PY310 {
|
||||
pyupgrade::rules::timeout_error_alias_call(checker, func);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::NonPEP604Isinstance) {
|
||||
if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
if checker.target_version() >= PythonVersion::PY310 {
|
||||
pyupgrade::rules::use_pep604_isinstance(checker, expr, func, args);
|
||||
}
|
||||
}
|
||||
|
@ -690,7 +690,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
);
|
||||
}
|
||||
if checker.enabled(Rule::ZipWithoutExplicitStrict) {
|
||||
if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
if checker.target_version() >= PythonVersion::PY310 {
|
||||
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
|
||||
}
|
||||
}
|
||||
|
@ -963,7 +963,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
flake8_pytest_style::rules::fail_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::ZipInsteadOfPairwise) {
|
||||
if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
if checker.target_version() >= PythonVersion::PY310 {
|
||||
ruff::rules::zip_instead_of_pairwise(checker, call);
|
||||
}
|
||||
}
|
||||
|
@ -1385,7 +1385,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
// Ex) `str | None`
|
||||
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::PY310
|
||||
&& checker.target_version() < PythonVersion::PY310
|
||||
&& checker.semantic.in_annotation()
|
||||
&& checker.semantic.in_runtime_evaluated_annotation()
|
||||
&& !checker.semantic.in_string_type_definition()
|
||||
|
|
|
@ -164,9 +164,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
|
||||
}
|
||||
}
|
||||
if checker.source_type.is_stub()
|
||||
|| checker.settings.target_version >= PythonVersion::PY311
|
||||
{
|
||||
if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY311 {
|
||||
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
|
||||
flake8_pyi::rules::no_return_argument_annotation(checker, parameters);
|
||||
}
|
||||
|
@ -194,12 +192,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
pylint::rules::global_statement(checker, name);
|
||||
}
|
||||
if checker.enabled(Rule::LRUCacheWithoutParameters) {
|
||||
if checker.settings.target_version >= PythonVersion::PY38 {
|
||||
if checker.target_version() >= PythonVersion::PY38 {
|
||||
pyupgrade::rules::lru_cache_without_parameters(checker, decorator_list);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::LRUCacheWithMaxsizeNone) {
|
||||
if checker.settings.target_version >= PythonVersion::PY39 {
|
||||
if checker.target_version() >= PythonVersion::PY39 {
|
||||
pyupgrade::rules::lru_cache_with_maxsize_none(checker, decorator_list);
|
||||
}
|
||||
}
|
||||
|
@ -445,7 +443,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
pyupgrade::rules::useless_object_inheritance(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::ReplaceStrEnum) {
|
||||
if checker.settings.target_version >= PythonVersion::PY311 {
|
||||
if checker.target_version() >= PythonVersion::PY311 {
|
||||
pyupgrade::rules::replace_str_enum(checker, class_def);
|
||||
}
|
||||
}
|
||||
|
@ -765,7 +763,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
}
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryFutureImport) {
|
||||
if checker.settings.target_version >= PythonVersion::PY37 {
|
||||
if checker.target_version() >= PythonVersion::PY37 {
|
||||
if let Some("__future__") = module {
|
||||
pyupgrade::rules::unnecessary_future_import(checker, stmt, names);
|
||||
}
|
||||
|
@ -1039,7 +1037,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
}
|
||||
}
|
||||
if checker.enabled(Rule::TimeoutErrorAlias) {
|
||||
if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
if checker.target_version() >= PythonVersion::PY310 {
|
||||
if let Some(item) = exc {
|
||||
pyupgrade::rules::timeout_error_alias_raise(checker, item);
|
||||
}
|
||||
|
@ -1431,7 +1429,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
|
||||
}
|
||||
if checker.enabled(Rule::ContinueInFinally) {
|
||||
if checker.settings.target_version <= PythonVersion::PY38 {
|
||||
if checker.target_version() <= PythonVersion::PY38 {
|
||||
pylint::rules::continue_in_finally(checker, finalbody);
|
||||
}
|
||||
}
|
||||
|
@ -1455,7 +1453,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
pyupgrade::rules::os_error_alias_handlers(checker, handlers);
|
||||
}
|
||||
if checker.enabled(Rule::TimeoutErrorAlias) {
|
||||
if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
if checker.target_version() >= PythonVersion::PY310 {
|
||||
pyupgrade::rules::timeout_error_alias_handlers(checker, handlers);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
|
|||
use ruff_python_ast::{
|
||||
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
|
||||
ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern,
|
||||
Stmt, Suite, UnaryOp,
|
||||
PythonVersion, Stmt, Suite, UnaryOp,
|
||||
};
|
||||
use ruff_python_ast::{helpers, str, visitor, PySourceType};
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
|
@ -223,6 +223,8 @@ pub(crate) struct Checker<'a> {
|
|||
last_stmt_end: TextSize,
|
||||
/// A state describing if a docstring is expected or not.
|
||||
docstring_state: DocstringState,
|
||||
/// The target [`PythonVersion`] for version-dependent checks
|
||||
target_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl<'a> Checker<'a> {
|
||||
|
@ -242,6 +244,7 @@ impl<'a> Checker<'a> {
|
|||
source_type: PySourceType,
|
||||
cell_offsets: Option<&'a CellOffsets>,
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
target_version: PythonVersion,
|
||||
) -> Checker<'a> {
|
||||
let mut semantic = SemanticModel::new(&settings.typing_modules, path, module);
|
||||
if settings.preview.is_enabled() {
|
||||
|
@ -272,6 +275,7 @@ impl<'a> Checker<'a> {
|
|||
notebook_index,
|
||||
last_stmt_end: TextSize::default(),
|
||||
docstring_state: DocstringState::default(),
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -500,6 +504,11 @@ impl<'a> Checker<'a> {
|
|||
self.report_diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`PythonVersion`] to use for version-related checks.
|
||||
pub(crate) const fn target_version(&self) -> PythonVersion {
|
||||
self.target_version
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for Checker<'a> {
|
||||
|
@ -2108,17 +2117,14 @@ impl<'a> Checker<'a> {
|
|||
}
|
||||
|
||||
fn bind_builtins(&mut self) {
|
||||
let target_version = self.target_version();
|
||||
let mut bind_builtin = |builtin| {
|
||||
// Add the builtin to the scope.
|
||||
let binding_id = self.semantic.push_builtin();
|
||||
let scope = self.semantic.global_scope_mut();
|
||||
scope.add(builtin, binding_id);
|
||||
};
|
||||
|
||||
let standard_builtins = python_builtins(
|
||||
self.settings.target_version.minor,
|
||||
self.source_type.is_ipynb(),
|
||||
);
|
||||
let standard_builtins = python_builtins(target_version.minor, self.source_type.is_ipynb());
|
||||
for builtin in standard_builtins {
|
||||
bind_builtin(builtin);
|
||||
}
|
||||
|
@ -2664,6 +2670,7 @@ pub(crate) fn check_ast(
|
|||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
notebook_index: Option<&NotebookIndex>,
|
||||
target_version: PythonVersion,
|
||||
) -> Vec<Diagnostic> {
|
||||
let module_path = package
|
||||
.map(PackageRoot::path)
|
||||
|
@ -2703,6 +2710,7 @@ pub(crate) fn check_ast(
|
|||
source_type,
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
target_version,
|
||||
);
|
||||
checker.bind_builtins();
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::path::Path;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
|
||||
use crate::package::PackageRoot;
|
||||
|
@ -17,6 +18,7 @@ pub(crate) fn check_file_path(
|
|||
locator: &Locator,
|
||||
comment_ranges: &CommentRanges,
|
||||
settings: &LinterSettings,
|
||||
target_version: PythonVersion,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
|
||||
|
@ -46,7 +48,7 @@ pub(crate) fn check_file_path(
|
|||
|
||||
// flake8-builtins
|
||||
if settings.rules.enabled(Rule::StdlibModuleShadowing) {
|
||||
if let Some(diagnostic) = stdlib_module_shadowing(path, settings) {
|
||||
if let Some(diagnostic) = stdlib_module_shadowing(path, settings, target_version) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_notebook::CellOffsets;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::{ModModule, PySourceType};
|
||||
use ruff_python_ast::{ModModule, PySourceType, PythonVersion};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::Parsed;
|
||||
|
@ -27,6 +27,7 @@ pub(crate) fn check_imports(
|
|||
package: Option<PackageRoot<'_>>,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
target_version: PythonVersion,
|
||||
) -> Vec<Diagnostic> {
|
||||
// Extract all import blocks from the AST.
|
||||
let tracker = {
|
||||
|
@ -52,6 +53,7 @@ pub(crate) fn check_imports(
|
|||
package,
|
||||
source_type,
|
||||
parsed.tokens(),
|
||||
target_version,
|
||||
) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::debug;
|
||||
use path_absolutize::Absolutize;
|
||||
|
||||
use crate::registry::RuleSet;
|
||||
|
@ -8,43 +7,8 @@ use crate::settings::types::CompiledPerFileIgnoreList;
|
|||
|
||||
/// Create a set with codes matching the pattern/code pairs.
|
||||
pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnoreList) -> RuleSet {
|
||||
let file_name = path.file_name().expect("Unable to parse filename");
|
||||
ignore_list
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if entry.basename_matcher.is_match(file_name) {
|
||||
if entry.negated { None } else {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
|
||||
path,
|
||||
entry.basename_matcher.glob().regex(),
|
||||
entry.rules
|
||||
);
|
||||
Some(&entry.rules)
|
||||
}
|
||||
} else if entry.absolute_matcher.is_match(path) {
|
||||
if entry.negated { None } else {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
|
||||
path,
|
||||
entry.absolute_matcher.glob().regex(),
|
||||
entry.rules
|
||||
);
|
||||
Some(&entry.rules)
|
||||
}
|
||||
} else if entry.negated {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}",
|
||||
path,
|
||||
entry.basename_matcher.glob().regex(),
|
||||
entry.absolute_matcher.glob().regex(),
|
||||
entry.rules
|
||||
);
|
||||
Some(&entry.rules)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.iter_matches(path, "Adding per-file ignores")
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -104,6 +104,8 @@ pub fn check_path(
|
|||
));
|
||||
}
|
||||
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
// Run the filesystem-based rules.
|
||||
if settings
|
||||
.rules
|
||||
|
@ -116,6 +118,7 @@ pub fn check_path(
|
|||
locator,
|
||||
comment_ranges,
|
||||
settings,
|
||||
target_version,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -158,6 +161,7 @@ pub fn check_path(
|
|||
source_type,
|
||||
cell_offsets,
|
||||
notebook_index,
|
||||
target_version,
|
||||
));
|
||||
}
|
||||
if use_imports {
|
||||
|
@ -171,6 +175,7 @@ pub fn check_path(
|
|||
package,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
target_version,
|
||||
);
|
||||
|
||||
diagnostics.extend(import_diagnostics);
|
||||
|
|
|
@ -399,7 +399,7 @@ impl ShadowedKind {
|
|||
|
||||
if is_python_builtin(
|
||||
new_name,
|
||||
checker.settings.target_version.minor,
|
||||
checker.target_version().minor,
|
||||
checker.source_type.is_ipynb(),
|
||||
) {
|
||||
return ShadowedKind::BuiltIn;
|
||||
|
|
|
@ -36,7 +36,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("fastapi").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
target_version: ruff_python_ast::PythonVersion::PY38,
|
||||
unresolved_target_version: ruff_python_ast::PythonVersion::PY38,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -226,13 +226,13 @@ fn create_diagnostic(
|
|||
) -> bool {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
FastApiNonAnnotatedDependency {
|
||||
py_version: checker.settings.target_version,
|
||||
py_version: checker.target_version(),
|
||||
},
|
||||
parameter.range,
|
||||
);
|
||||
|
||||
let try_generate_fix = || {
|
||||
let module = if checker.settings.target_version >= PythonVersion::PY39 {
|
||||
let module = if checker.target_version() >= PythonVersion::PY39 {
|
||||
"typing"
|
||||
} else {
|
||||
"typing_extensions"
|
||||
|
|
|
@ -128,7 +128,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_annotations/auto_return_type.py"),
|
||||
&LinterSettings {
|
||||
target_version: PythonVersion::PY38,
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
..LinterSettings::for_rules(vec![
|
||||
Rule::MissingReturnTypeUndocumentedPublicFunction,
|
||||
Rule::MissingReturnTypePrivateFunction,
|
||||
|
|
|
@ -523,7 +523,7 @@ fn check_dynamically_typed<F>(
|
|||
if type_hint_resolves_to_any(
|
||||
parsed_annotation.expression(),
|
||||
checker,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
AnyType { name: func() },
|
||||
|
@ -532,7 +532,7 @@ fn check_dynamically_typed<F>(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if type_hint_resolves_to_any(annotation, checker, checker.settings.target_version) {
|
||||
if type_hint_resolves_to_any(annotation, checker, checker.target_version()) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
AnyType { name: func() },
|
||||
annotation.range(),
|
||||
|
@ -725,7 +725,7 @@ pub(crate) fn definition(
|
|||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
|
||||
|
@ -756,7 +756,7 @@ pub(crate) fn definition(
|
|||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
|
||||
|
@ -826,7 +826,7 @@ pub(crate) fn definition(
|
|||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
|
@ -865,7 +865,7 @@ pub(crate) fn definition(
|
|||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
|
|
|
@ -44,7 +44,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_async").join(path),
|
||||
&LinterSettings {
|
||||
target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -108,7 +108,7 @@ pub(crate) fn async_function_with_timeout(checker: &Checker, function_def: &ast:
|
|||
};
|
||||
|
||||
// asyncio.timeout feature was first introduced in Python 3.11
|
||||
if module == AsyncModule::AsyncIo && checker.settings.target_version < PythonVersion::PY311 {
|
||||
if module == AsyncModule::AsyncIo && checker.target_version() < PythonVersion::PY311 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_bugbear").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
target_version,
|
||||
unresolved_target_version: target_version,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -59,7 +59,7 @@ impl Violation for BatchedWithoutExplicitStrict {
|
|||
|
||||
/// B911
|
||||
pub(crate) fn batched_without_explicit_strict(checker: &Checker, call: &ExprCall) {
|
||||
if checker.settings.target_version < PythonVersion::PY313 {
|
||||
if checker.target_version() < PythonVersion::PY313 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl
|
|||
// skip `self`
|
||||
.skip(1)
|
||||
.all(|param| param.annotation().is_some() && !param.is_variadic())
|
||||
&& (func_def.parameters.kwonlyargs.is_empty() || checker.settings.target_version >= PythonVersion::PY310)
|
||||
&& (func_def.parameters.kwonlyargs.is_empty() || checker.target_version() >= PythonVersion::PY310)
|
||||
// `__init__` should not have complicated logic in it
|
||||
// only assignments
|
||||
&& func_def
|
||||
|
|
|
@ -217,7 +217,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_builtins").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
target_version: PythonVersion::PY38,
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -68,7 +68,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &Checker, parameter: &Paramete
|
|||
parameter.name(),
|
||||
checker.source_type,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
) {
|
||||
// Ignore parameters in lambda expressions.
|
||||
// (That is the domain of A006.)
|
||||
|
|
|
@ -99,7 +99,7 @@ pub(crate) fn builtin_attribute_shadowing(
|
|||
name,
|
||||
checker.source_type,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
) {
|
||||
// Ignore explicit overrides.
|
||||
if class_def.decorator_list.iter().any(|decorator| {
|
||||
|
|
|
@ -61,7 +61,7 @@ pub(crate) fn builtin_import_shadowing(checker: &Checker, alias: &Alias) {
|
|||
name.as_str(),
|
||||
checker.source_type,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
) {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
BuiltinImportShadowing {
|
||||
|
|
|
@ -44,7 +44,7 @@ pub(crate) fn builtin_lambda_argument_shadowing(checker: &Checker, lambda: &Expr
|
|||
name,
|
||||
checker.source_type,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
) {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
BuiltinLambdaArgumentShadowing {
|
||||
|
|
|
@ -63,7 +63,7 @@ pub(crate) fn builtin_variable_shadowing(checker: &Checker, name: &str, range: T
|
|||
name,
|
||||
checker.source_type,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
) {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
BuiltinVariableShadowing {
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::path::{Component, Path, PathBuf};
|
|||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_ast::{PySourceType, PythonVersion};
|
||||
use ruff_python_stdlib::path::is_module_file;
|
||||
use ruff_python_stdlib::sys::is_known_standard_library;
|
||||
use ruff_text_size::TextRange;
|
||||
|
@ -69,6 +69,7 @@ impl Violation for StdlibModuleShadowing {
|
|||
pub(crate) fn stdlib_module_shadowing(
|
||||
mut path: &Path,
|
||||
settings: &LinterSettings,
|
||||
target_version: PythonVersion,
|
||||
) -> Option<Diagnostic> {
|
||||
if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file) {
|
||||
return None;
|
||||
|
@ -98,7 +99,7 @@ pub(crate) fn stdlib_module_shadowing(
|
|||
|
||||
let module_name = components.next()?;
|
||||
|
||||
if is_allowed_module(settings, &module_name) {
|
||||
if is_allowed_module(settings, target_version, &module_name) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -129,7 +130,7 @@ fn get_prefix<'a>(settings: &'a LinterSettings, path: &Path) -> Option<&'a PathB
|
|||
prefix
|
||||
}
|
||||
|
||||
fn is_allowed_module(settings: &LinterSettings, module: &str) -> bool {
|
||||
fn is_allowed_module(settings: &LinterSettings, version: PythonVersion, module: &str) -> bool {
|
||||
// Shadowing private stdlib modules is okay.
|
||||
// https://github.com/astral-sh/ruff/issues/12949
|
||||
if module.starts_with('_') && !module.starts_with("__") {
|
||||
|
@ -145,5 +146,5 @@ fn is_allowed_module(settings: &LinterSettings, module: &str) -> bool {
|
|||
return true;
|
||||
}
|
||||
|
||||
!is_known_standard_library(settings.target_version.minor, module)
|
||||
!is_known_standard_library(version.minor, module)
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_future_annotations").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation)
|
||||
},
|
||||
)?;
|
||||
|
@ -49,7 +49,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_future_annotations").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -189,7 +189,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_pyi").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY38,
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -560,7 +560,7 @@ fn replace_custom_typevar_with_self(
|
|||
/// This is because it was added to the `typing` module on Python 3.11,
|
||||
/// but is available from the backport package `typing_extensions` on all versions.
|
||||
fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String), ResolutionError> {
|
||||
let source_module = if checker.settings.target_version >= PythonVersion::PY311 {
|
||||
let source_module = if checker.target_version() >= PythonVersion::PY311 {
|
||||
"typing"
|
||||
} else {
|
||||
"typing_extensions"
|
||||
|
|
|
@ -67,7 +67,7 @@ pub(crate) fn no_return_argument_annotation(checker: &Checker, parameters: &ast:
|
|||
if is_no_return(annotation, checker) {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
NoReturnArgumentAnnotationInStub {
|
||||
module: if checker.settings.target_version >= PythonVersion::PY311 {
|
||||
module: if checker.target_version() >= PythonVersion::PY311 {
|
||||
TypingModule::Typing
|
||||
} else {
|
||||
TypingModule::TypingExtensions
|
||||
|
|
|
@ -215,7 +215,7 @@ fn replace_with_self_fix(
|
|||
let semantic = checker.semantic();
|
||||
|
||||
let (self_import, self_binding) = {
|
||||
let source_module = if checker.settings.target_version >= PythonVersion::PY311 {
|
||||
let source_module = if checker.target_version() >= PythonVersion::PY311 {
|
||||
"typing"
|
||||
} else {
|
||||
"typing_extensions"
|
||||
|
|
|
@ -56,7 +56,7 @@ impl Violation for Pep484StylePositionalOnlyParameter {
|
|||
/// PYI063
|
||||
pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||
// PEP 570 was introduced in Python 3.8.
|
||||
if checker.settings.target_version < PythonVersion::PY38 {
|
||||
if checker.target_version() < PythonVersion::PY38 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -112,9 +112,7 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex
|
|||
|
||||
let union_kind = if literal_elements.is_empty() {
|
||||
UnionKind::NoUnion
|
||||
} else if (checker.settings.target_version >= PythonVersion::PY310)
|
||||
|| checker.source_type.is_stub()
|
||||
{
|
||||
} else if (checker.target_version() >= PythonVersion::PY310) || checker.source_type.is_stub() {
|
||||
UnionKind::BitOr
|
||||
} else {
|
||||
UnionKind::TypingOptional
|
||||
|
|
|
@ -667,7 +667,7 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar
|
|||
return;
|
||||
}
|
||||
|
||||
let module = if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
let module = if checker.target_version() >= PythonVersion::PY310 {
|
||||
TypingModule::Typing
|
||||
} else {
|
||||
TypingModule::TypingExtensions
|
||||
|
|
|
@ -3,7 +3,7 @@ use flake8_quotes::settings::Quote;
|
|||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::visitor::{walk_f_string, Visitor};
|
||||
use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike};
|
||||
use ruff_python_ast::{self as ast, AnyStringFlags, PythonVersion, StringFlags, StringLike};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
@ -61,7 +61,11 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
|
|||
return;
|
||||
}
|
||||
|
||||
let mut rule_checker = AvoidableEscapedQuoteChecker::new(checker.locator(), checker.settings);
|
||||
let mut rule_checker = AvoidableEscapedQuoteChecker::new(
|
||||
checker.locator(),
|
||||
checker.settings,
|
||||
checker.target_version(),
|
||||
);
|
||||
|
||||
for part in string_like.parts() {
|
||||
match part {
|
||||
|
@ -88,11 +92,15 @@ struct AvoidableEscapedQuoteChecker<'a> {
|
|||
}
|
||||
|
||||
impl<'a> AvoidableEscapedQuoteChecker<'a> {
|
||||
fn new(locator: &'a Locator<'a>, settings: &'a LinterSettings) -> Self {
|
||||
fn new(
|
||||
locator: &'a Locator<'a>,
|
||||
settings: &'a LinterSettings,
|
||||
target_version: PythonVersion,
|
||||
) -> Self {
|
||||
Self {
|
||||
locator,
|
||||
quotes_settings: &settings.flake8_quotes,
|
||||
supports_pep701: settings.target_version.supports_pep_701(),
|
||||
supports_pep701: target_version.supports_pep_701(),
|
||||
diagnostics: vec![],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY39,
|
||||
unresolved_target_version: PythonVersion::PY39,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -10,7 +10,6 @@ use ruff_text_size::Ranged;
|
|||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_type_checking::helpers::quote_type_expression;
|
||||
use crate::settings::LinterSettings;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
/// ## What it does
|
||||
|
@ -284,7 +283,7 @@ pub(crate) fn quoted_type_alias(
|
|||
|
||||
// explicit type aliases require some additional checks to avoid false positives
|
||||
if checker.semantic().in_annotated_type_alias_value()
|
||||
&& quotes_are_unremovable(checker.semantic(), expr, checker.settings)
|
||||
&& quotes_are_unremovable(checker.semantic(), expr, checker.target_version())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -305,7 +304,7 @@ pub(crate) fn quoted_type_alias(
|
|||
fn quotes_are_unremovable(
|
||||
semantic: &SemanticModel,
|
||||
expr: &Expr,
|
||||
settings: &LinterSettings,
|
||||
target_version: PythonVersion,
|
||||
) -> bool {
|
||||
match expr {
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
|
@ -313,11 +312,11 @@ fn quotes_are_unremovable(
|
|||
}) => {
|
||||
match op {
|
||||
Operator::BitOr => {
|
||||
if settings.target_version < PythonVersion::PY310 {
|
||||
if target_version < PythonVersion::PY310 {
|
||||
return true;
|
||||
}
|
||||
quotes_are_unremovable(semantic, left, settings)
|
||||
|| quotes_are_unremovable(semantic, right, settings)
|
||||
quotes_are_unremovable(semantic, left, target_version)
|
||||
|| quotes_are_unremovable(semantic, right, target_version)
|
||||
}
|
||||
// for now we'll treat uses of other operators as unremovable quotes
|
||||
// since that would make it an invalid type expression anyways. We skip
|
||||
|
@ -330,7 +329,7 @@ fn quotes_are_unremovable(
|
|||
value,
|
||||
ctx: ExprContext::Load,
|
||||
..
|
||||
}) => quotes_are_unremovable(semantic, value, settings),
|
||||
}) => quotes_are_unremovable(semantic, value, target_version),
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
// for subscripts we don't know whether it's safe to do at runtime
|
||||
// since the operation may only be available at type checking time.
|
||||
|
@ -338,7 +337,7 @@ fn quotes_are_unremovable(
|
|||
if !semantic.in_type_checking_block() {
|
||||
return true;
|
||||
}
|
||||
if quotes_are_unremovable(semantic, value, settings) {
|
||||
if quotes_are_unremovable(semantic, value, target_version) {
|
||||
return true;
|
||||
}
|
||||
// for `typing.Annotated`, only analyze the first argument, since the rest may
|
||||
|
@ -347,23 +346,23 @@ fn quotes_are_unremovable(
|
|||
if semantic.match_typing_qualified_name(&qualified_name, "Annotated") {
|
||||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
|
||||
return !elts.is_empty()
|
||||
&& quotes_are_unremovable(semantic, &elts[0], settings);
|
||||
&& quotes_are_unremovable(semantic, &elts[0], target_version);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
quotes_are_unremovable(semantic, slice, settings)
|
||||
quotes_are_unremovable(semantic, slice, target_version)
|
||||
}
|
||||
Expr::Attribute(ast::ExprAttribute { value, .. }) => {
|
||||
// for attributes we also don't know whether it's safe
|
||||
if !semantic.in_type_checking_block() {
|
||||
return true;
|
||||
}
|
||||
quotes_are_unremovable(semantic, value, settings)
|
||||
quotes_are_unremovable(semantic, value, target_version)
|
||||
}
|
||||
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
for elt in elts {
|
||||
if quotes_are_unremovable(semantic, elt, settings) {
|
||||
if quotes_are_unremovable(semantic, elt, target_version) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -307,7 +307,7 @@ pub(crate) fn typing_only_runtime_import(
|
|||
checker.package(),
|
||||
checker.settings.isort.detect_same_package,
|
||||
&checker.settings.isort.known_modules,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
checker.settings.isort.no_sections,
|
||||
&checker.settings.isort.section_order,
|
||||
&checker.settings.isort.default_section,
|
||||
|
|
|
@ -152,7 +152,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
|||
),
|
||||
// PTH115
|
||||
// Python 3.9+
|
||||
["os", "readlink"] if checker.settings.target_version >= PythonVersion::PY39 => {
|
||||
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
|
||||
Some(OsReadlink.into())
|
||||
}
|
||||
// PTH208,
|
||||
|
|
|
@ -3,7 +3,7 @@ use itertools::{EitherOrBoth, Itertools};
|
|||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::whitespace::trailing_lines_end;
|
||||
use ruff_python_ast::{PySourceType, Stmt};
|
||||
use ruff_python_ast::{PySourceType, PythonVersion, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::Tokens;
|
||||
|
@ -88,6 +88,7 @@ pub(crate) fn organize_imports(
|
|||
package: Option<PackageRoot<'_>>,
|
||||
source_type: PySourceType,
|
||||
tokens: &Tokens,
|
||||
target_version: PythonVersion,
|
||||
) -> Option<Diagnostic> {
|
||||
let indentation = locator.slice(extract_indentation_range(&block.imports, locator));
|
||||
let indentation = leading_indentation(indentation);
|
||||
|
@ -127,7 +128,7 @@ pub(crate) fn organize_imports(
|
|||
&settings.src,
|
||||
package,
|
||||
source_type,
|
||||
settings.target_version,
|
||||
target_version,
|
||||
&settings.isort,
|
||||
tokens,
|
||||
);
|
||||
|
|
|
@ -43,7 +43,7 @@ mod tests {
|
|||
Path::new("perflint").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -89,7 +89,7 @@ impl Violation for TryExceptInLoop {
|
|||
|
||||
/// PERF203
|
||||
pub(crate) fn try_except_in_loop(checker: &Checker, body: &[Stmt]) {
|
||||
if checker.settings.target_version >= PythonVersion::PY311 {
|
||||
if checker.target_version() >= PythonVersion::PY311 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -218,7 +218,7 @@ mod tests {
|
|||
let diagnostics = test_snippet(
|
||||
"PythonFinalizationError",
|
||||
&LinterSettings {
|
||||
target_version: ruff_python_ast::PythonVersion::PY312,
|
||||
unresolved_target_version: ruff_python_ast::PythonVersion::PY312,
|
||||
..LinterSettings::for_rule(Rule::UndefinedName)
|
||||
},
|
||||
);
|
||||
|
|
|
@ -229,7 +229,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
|
|||
checker.package(),
|
||||
checker.settings.isort.detect_same_package,
|
||||
&checker.settings.isort.known_modules,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
checker.settings.isort.no_sections,
|
||||
&checker.settings.isort.section_order,
|
||||
&checker.settings.isort.default_section,
|
||||
|
|
|
@ -211,7 +211,7 @@ pub(crate) fn bad_str_strip_call(checker: &Checker, call: &ast::ExprCall) {
|
|||
return;
|
||||
}
|
||||
|
||||
let removal = if checker.settings.target_version >= PythonVersion::PY39 {
|
||||
let removal = if checker.target_version() >= PythonVersion::PY39 {
|
||||
RemovalKind::for_strip(strip)
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -76,7 +76,7 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) {
|
|||
}
|
||||
|
||||
// If this is an allowed dunder method, abort.
|
||||
if allowed_dunder_constants(attr, checker.settings.target_version) {
|
||||
if allowed_dunder_constants(attr, checker.target_version()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExp
|
|||
return;
|
||||
};
|
||||
|
||||
if is_builtin_exception(func, checker.semantic(), checker.settings.target_version) {
|
||||
if is_builtin_exception(func, checker.semantic(), checker.target_version()) {
|
||||
let mut diagnostic = Diagnostic::new(UselessExceptionStatement, expr.range());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
|
||||
"raise ".to_string(),
|
||||
|
|
|
@ -156,7 +156,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP041.py"),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias)
|
||||
},
|
||||
)?;
|
||||
|
@ -169,7 +169,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP040.py"),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias)
|
||||
},
|
||||
)?;
|
||||
|
@ -185,7 +185,7 @@ mod tests {
|
|||
pyupgrade: pyupgrade::settings::Settings {
|
||||
keep_runtime_typing: true,
|
||||
},
|
||||
target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
|
@ -201,7 +201,7 @@ mod tests {
|
|||
pyupgrade: pyupgrade::settings::Settings {
|
||||
keep_runtime_typing: true,
|
||||
},
|
||||
target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
|
@ -214,7 +214,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
|
@ -227,7 +227,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
|
||||
},
|
||||
)?;
|
||||
|
@ -240,7 +240,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY37,
|
||||
unresolved_target_version: PythonVersion::PY37,
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::NonPEP604AnnotationUnion,
|
||||
Rule::NonPEP604AnnotationOptional,
|
||||
|
@ -256,7 +256,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/future_annotations.py"),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
..settings::LinterSettings::for_rules([
|
||||
Rule::NonPEP604AnnotationUnion,
|
||||
Rule::NonPEP604AnnotationOptional,
|
||||
|
@ -272,7 +272,7 @@ mod tests {
|
|||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP017.py"),
|
||||
&settings::LinterSettings {
|
||||
target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC)
|
||||
},
|
||||
)?;
|
||||
|
@ -286,7 +286,7 @@ mod tests {
|
|||
Path::new("pyupgrade/UP044.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
target_version: PythonVersion::PY311,
|
||||
unresolved_target_version: PythonVersion::PY311,
|
||||
..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -722,7 +722,7 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
|
|||
checker.locator(),
|
||||
checker.stylist(),
|
||||
checker.tokens(),
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
);
|
||||
|
||||
for (operation, fix) in fixer.without_renames() {
|
||||
|
|
|
@ -115,7 +115,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) {
|
|||
let Some(version) = extract_version(elts) else {
|
||||
return;
|
||||
};
|
||||
let target = checker.settings.target_version;
|
||||
let target = checker.target_version();
|
||||
match version_always_less_than(
|
||||
&version,
|
||||
target,
|
||||
|
|
|
@ -106,7 +106,7 @@ impl Violation for NonPEP695GenericClass {
|
|||
/// UP046
|
||||
pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassDef) {
|
||||
// PEP-695 syntax is only available on Python 3.12+
|
||||
if checker.settings.target_version < PythonVersion::PY312 {
|
||||
if checker.target_version() < PythonVersion::PY312 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ impl Violation for NonPEP695GenericFunction {
|
|||
/// UP047
|
||||
pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &StmtFunctionDef) {
|
||||
// PEP-695 syntax is only available on Python 3.12+
|
||||
if checker.settings.target_version < PythonVersion::PY312 {
|
||||
if checker.target_version() < PythonVersion::PY312 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ impl Violation for NonPEP695TypeAlias {
|
|||
|
||||
/// UP040
|
||||
pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) {
|
||||
if checker.settings.target_version < PythonVersion::PY312 {
|
||||
if checker.target_version() < PythonVersion::PY312 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -182,7 +182,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) {
|
|||
|
||||
/// UP040
|
||||
pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) {
|
||||
if checker.settings.target_version < PythonVersion::PY312 {
|
||||
if checker.target_version() < PythonVersion::PY312 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -162,7 +162,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
|
|||
};
|
||||
match expr.as_ref() {
|
||||
Expr::Name(_) | Expr::Attribute(_) => {
|
||||
if is_alias(expr, checker.semantic(), checker.settings.target_version) {
|
||||
if is_alias(expr, checker.semantic(), checker.target_version()) {
|
||||
atom_diagnostic(checker, expr);
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
|
|||
// List of aliases to replace with `TimeoutError`.
|
||||
let mut aliases: Vec<&Expr> = vec![];
|
||||
for element in tuple {
|
||||
if is_alias(element, checker.semantic(), checker.settings.target_version) {
|
||||
if is_alias(element, checker.semantic(), checker.target_version()) {
|
||||
aliases.push(element);
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
|
|||
|
||||
/// UP041
|
||||
pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) {
|
||||
if is_alias(func, checker.semantic(), checker.settings.target_version) {
|
||||
if is_alias(func, checker.semantic(), checker.target_version()) {
|
||||
atom_diagnostic(checker, func);
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) {
|
|||
/// UP041
|
||||
pub(crate) fn timeout_error_alias_raise(checker: &Checker, expr: &Expr) {
|
||||
if matches!(expr, Expr::Name(_) | Expr::Attribute(_)) {
|
||||
if is_alias(expr, checker.semantic(), checker.settings.target_version) {
|
||||
if is_alias(expr, checker.semantic(), checker.target_version()) {
|
||||
atom_diagnostic(checker, expr);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement:
|
|||
checker.semantic(),
|
||||
)?;
|
||||
let binding_edit = Edit::range_replacement(binding, expr.range());
|
||||
let applicability = if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
let applicability = if checker.target_version() >= PythonVersion::PY310 {
|
||||
Applicability::Safe
|
||||
} else {
|
||||
Applicability::Unsafe
|
||||
|
@ -122,7 +122,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement:
|
|||
Ok(Fix::applicable_edits(
|
||||
import_edit,
|
||||
[reference_edit],
|
||||
if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
if checker.target_version() >= PythonVersion::PY310 {
|
||||
Applicability::Safe
|
||||
} else {
|
||||
Applicability::Unsafe
|
||||
|
|
|
@ -142,7 +142,7 @@ pub(crate) fn non_pep604_annotation(
|
|||
&& !checker.semantic().in_complex_string_type_definition()
|
||||
&& is_allowed_value(slice);
|
||||
|
||||
let applicability = if checker.settings.target_version >= PythonVersion::PY310 {
|
||||
let applicability = if checker.target_version() >= PythonVersion::PY310 {
|
||||
Applicability::Safe
|
||||
} else {
|
||||
Applicability::Unsafe
|
||||
|
|
|
@ -53,7 +53,7 @@ impl Violation for NonPEP646Unpack {
|
|||
|
||||
/// UP044
|
||||
pub(crate) fn use_pep646_unpack(checker: &Checker, expr: &ExprSubscript) {
|
||||
if checker.settings.target_version < PythonVersion::PY311 {
|
||||
if checker.target_version() < PythonVersion::PY311 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ impl AlwaysFixableViolation for BitCount {
|
|||
/// FURB161
|
||||
pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) {
|
||||
// `int.bit_count()` was added in Python 3.10
|
||||
if checker.settings.target_version < PythonVersion::PY310 {
|
||||
if checker.target_version() < PythonVersion::PY310 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ impl AlwaysFixableViolation for FromisoformatReplaceZ {
|
|||
|
||||
/// FURB162
|
||||
pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) {
|
||||
if checker.settings.target_version < PythonVersion::PY311 {
|
||||
if checker.target_version() < PythonVersion::PY311 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -58,12 +58,7 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) {
|
|||
}
|
||||
|
||||
// First we go through all the items in the statement and find all `open` operations.
|
||||
let candidates = find_file_opens(
|
||||
with,
|
||||
checker.semantic(),
|
||||
true,
|
||||
checker.settings.target_version,
|
||||
);
|
||||
let candidates = find_file_opens(with, checker.semantic(), true, checker.target_version());
|
||||
if candidates.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ impl AlwaysFixableViolation for SliceToRemovePrefixOrSuffix {
|
|||
|
||||
/// FURB188
|
||||
pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprIf) {
|
||||
if checker.settings.target_version < PythonVersion::PY39 {
|
||||
if checker.target_version() < PythonVersion::PY39 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI
|
|||
|
||||
/// FURB188
|
||||
pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtIf) {
|
||||
if checker.settings.target_version < PythonVersion::PY39 {
|
||||
if checker.target_version() < PythonVersion::PY39 {
|
||||
return;
|
||||
}
|
||||
if let Some(removal_data) = affix_removal_data_stmt(if_stmt) {
|
||||
|
|
|
@ -59,12 +59,7 @@ pub(crate) fn write_whole_file(checker: &Checker, with: &ast::StmtWith) {
|
|||
}
|
||||
|
||||
// First we go through all the items in the statement and find all `open` operations.
|
||||
let candidates = find_file_opens(
|
||||
with,
|
||||
checker.semantic(),
|
||||
false,
|
||||
checker.settings.target_version,
|
||||
);
|
||||
let candidates = find_file_opens(with, checker.semantic(), false, checker.target_version());
|
||||
if candidates.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ mod tests {
|
|||
extend_markup_names: vec![],
|
||||
allowed_markup_calls: vec![],
|
||||
},
|
||||
target_version: PythonVersion::PY310,
|
||||
unresolved_target_version: PythonVersion::PY310,
|
||||
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
|
||||
},
|
||||
)?;
|
||||
|
|
|
@ -79,7 +79,7 @@ impl Violation for ClassWithMixedTypeVars {
|
|||
|
||||
/// RUF053
|
||||
pub(crate) fn class_with_mixed_type_vars(checker: &Checker, class_def: &StmtClassDef) {
|
||||
if checker.settings.target_version < PythonVersion::PY312 {
|
||||
if checker.target_version() < PythonVersion::PY312 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -177,11 +177,11 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
|
|||
let Some(expr) = type_hint_explicitly_allows_none(
|
||||
parsed_annotation.expression(),
|
||||
checker,
|
||||
checker.settings.target_version,
|
||||
checker.target_version(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let conversion_type = checker.settings.target_version.into();
|
||||
let conversion_type = checker.target_version().into();
|
||||
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());
|
||||
|
@ -192,14 +192,12 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
|
|||
}
|
||||
} else {
|
||||
// Unquoted annotation.
|
||||
let Some(expr) = type_hint_explicitly_allows_none(
|
||||
annotation,
|
||||
checker,
|
||||
checker.settings.target_version,
|
||||
) else {
|
||||
let Some(expr) =
|
||||
type_hint_explicitly_allows_none(annotation, checker, checker.target_version())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let conversion_type = checker.settings.target_version.into();
|
||||
let conversion_type = checker.target_version().into();
|
||||
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());
|
||||
|
|
|
@ -88,7 +88,7 @@ pub(crate) fn subscript_with_parenthesized_tuple(checker: &Checker, subscript: &
|
|||
// to a syntax error in Python 3.10.
|
||||
// This is no longer a syntax error starting in Python 3.11
|
||||
// see https://peps.python.org/pep-0646/#change-1-star-expressions-in-indexes
|
||||
if checker.settings.target_version <= PythonVersion::PY310
|
||||
if checker.target_version() <= PythonVersion::PY310
|
||||
&& !prefer_parentheses
|
||||
&& tuple_subscript.iter().any(Expr::is_starred_expr)
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@ use rustc_hash::FxHashSet;
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use types::CompiledPerFileTargetVersionList;
|
||||
|
||||
use crate::codes::RuleCodePrefix;
|
||||
use ruff_macros::CacheKey;
|
||||
|
@ -219,7 +220,21 @@ pub struct LinterSettings {
|
|||
pub per_file_ignores: CompiledPerFileIgnoreList,
|
||||
pub fix_safety: FixSafetyTable,
|
||||
|
||||
pub target_version: PythonVersion,
|
||||
/// The non-path-resolved Python version specified by the `target-version` input option.
|
||||
///
|
||||
/// If you have a `Checker` available, see its `target_version` method instead.
|
||||
///
|
||||
/// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to obtain the Python
|
||||
/// version for a given file, while respecting the overrides in `per_file_target_version`.
|
||||
pub unresolved_target_version: PythonVersion,
|
||||
/// Path-specific overrides to `unresolved_target_version`.
|
||||
///
|
||||
/// If you have a `Checker` available, see its `target_version` method instead.
|
||||
///
|
||||
/// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to check a given
|
||||
/// [`Path`] against these patterns, while falling back to `unresolved_target_version` if none
|
||||
/// of them match.
|
||||
pub per_file_target_version: CompiledPerFileTargetVersionList,
|
||||
pub preview: PreviewMode,
|
||||
pub explicit_preview_rules: bool,
|
||||
|
||||
|
@ -281,7 +296,8 @@ impl Display for LinterSettings {
|
|||
self.per_file_ignores,
|
||||
self.fix_safety | nested,
|
||||
|
||||
self.target_version,
|
||||
self.unresolved_target_version,
|
||||
self.per_file_target_version,
|
||||
self.preview,
|
||||
self.explicit_preview_rules,
|
||||
self.extension | debug,
|
||||
|
@ -361,7 +377,7 @@ impl LinterSettings {
|
|||
pub fn for_rule(rule_code: Rule) -> Self {
|
||||
Self {
|
||||
rules: RuleTable::from_iter([rule_code]),
|
||||
target_version: PythonVersion::latest(),
|
||||
unresolved_target_version: PythonVersion::latest(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
@ -369,7 +385,7 @@ impl LinterSettings {
|
|||
pub fn for_rules(rules: impl IntoIterator<Item = Rule>) -> Self {
|
||||
Self {
|
||||
rules: RuleTable::from_iter(rules),
|
||||
target_version: PythonVersion::latest(),
|
||||
unresolved_target_version: PythonVersion::latest(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
@ -377,7 +393,8 @@ impl LinterSettings {
|
|||
pub fn new(project_root: &Path) -> Self {
|
||||
Self {
|
||||
exclude: FilePatternSet::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
unresolved_target_version: PythonVersion::default(),
|
||||
per_file_target_version: CompiledPerFileTargetVersionList::default(),
|
||||
project_root: project_root.to_path_buf(),
|
||||
rules: DEFAULT_SELECTORS
|
||||
.iter()
|
||||
|
@ -439,9 +456,20 @@ impl LinterSettings {
|
|||
|
||||
#[must_use]
|
||||
pub fn with_target_version(mut self, target_version: PythonVersion) -> Self {
|
||||
self.target_version = target_version;
|
||||
self.unresolved_target_version = target_version;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resolve the [`PythonVersion`] to use for linting.
|
||||
///
|
||||
/// This method respects the per-file version overrides in
|
||||
/// [`LinterSettings::per_file_target_version`] and falls back on
|
||||
/// [`LinterSettings::unresolved_target_version`] if none of the override patterns match.
|
||||
pub fn resolve_target_version(&self, path: &Path) -> PythonVersion {
|
||||
self.per_file_target_version
|
||||
.is_match(path)
|
||||
.unwrap_or(self.unresolved_target_version)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LinterSettings {
|
||||
|
|
|
@ -5,8 +5,9 @@ use std::path::{Path, PathBuf};
|
|||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use log::debug;
|
||||
use pep440_rs::{VersionSpecifier, VersionSpecifiers};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
|
@ -274,22 +275,25 @@ impl CacheKey for FilePatternSet {
|
|||
}
|
||||
}
|
||||
|
||||
/// A glob pattern and associated data for matching file paths.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PerFileIgnore {
|
||||
pub(crate) basename: String,
|
||||
pub(crate) absolute: PathBuf,
|
||||
pub(crate) negated: bool,
|
||||
pub(crate) rules: RuleSet,
|
||||
pub struct PerFile<T> {
|
||||
/// The glob pattern used to construct the [`PerFile`].
|
||||
basename: String,
|
||||
/// The same pattern as `basename` but normalized to the project root directory.
|
||||
absolute: PathBuf,
|
||||
/// Whether the glob pattern should be negated (e.g. `!*.ipynb`)
|
||||
negated: bool,
|
||||
/// The per-file data associated with these glob patterns.
|
||||
data: T,
|
||||
}
|
||||
|
||||
impl PerFileIgnore {
|
||||
pub fn new(
|
||||
mut pattern: String,
|
||||
prefixes: &[RuleSelector],
|
||||
project_root: Option<&Path>,
|
||||
) -> Self {
|
||||
// Rules in preview are included here even if preview mode is disabled; it's safe to ignore disabled rules
|
||||
let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect();
|
||||
impl<T> PerFile<T> {
|
||||
/// Construct a new [`PerFile`] from the given glob `pattern` and containing `data`.
|
||||
///
|
||||
/// If provided, `project_root` is used to construct a second glob pattern normalized to the
|
||||
/// project root directory. See [`fs::normalize_path_to`] for more details.
|
||||
fn new(mut pattern: String, project_root: Option<&Path>, data: T) -> Self {
|
||||
let negated = pattern.starts_with('!');
|
||||
if negated {
|
||||
pattern.drain(..1);
|
||||
|
@ -304,11 +308,26 @@ impl PerFileIgnore {
|
|||
basename: pattern,
|
||||
absolute,
|
||||
negated,
|
||||
rules,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-file ignored linting rules.
|
||||
///
|
||||
/// See [`PerFile`] for details of the representation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PerFileIgnore(PerFile<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)]
|
||||
pub struct PatternPrefixPair {
|
||||
pub pattern: String,
|
||||
|
@ -558,15 +577,47 @@ impl Display for RequiredVersion {
|
|||
/// pattern matching.
|
||||
pub type IdentifierPattern = glob::Pattern;
|
||||
|
||||
#[derive(Debug, Clone, CacheKey)]
|
||||
pub struct CompiledPerFileIgnore {
|
||||
/// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompiledPerFile<T> {
|
||||
pub absolute_matcher: GlobMatcher,
|
||||
pub basename_matcher: GlobMatcher,
|
||||
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 {
|
||||
display_settings! {
|
||||
formatter = f,
|
||||
|
@ -574,52 +625,130 @@ impl Display for CompiledPerFileIgnore {
|
|||
self.absolute_matcher | globmatcher,
|
||||
self.basename_matcher | globmatcher,
|
||||
self.negated,
|
||||
self.rules,
|
||||
self.data,
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, CacheKey, Default)]
|
||||
pub struct CompiledPerFileIgnoreList {
|
||||
// Ordered as (absolute path matcher, basename matcher, rules)
|
||||
ignores: Vec<CompiledPerFileIgnore>,
|
||||
/// A sequence of [`CompiledPerFile<T>`].
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CompiledPerFileList<T> {
|
||||
inner: Vec<CompiledPerFile<T>>,
|
||||
}
|
||||
|
||||
impl CompiledPerFileIgnoreList {
|
||||
/// Given a list of patterns, create a `GlobSet`.
|
||||
pub fn resolve(per_file_ignores: Vec<PerFileIgnore>) -> Result<Self> {
|
||||
let ignores: Result<Vec<_>> = per_file_ignores
|
||||
.into_iter()
|
||||
.map(|per_file_ignore| {
|
||||
// Construct absolute path matcher.
|
||||
let absolute_matcher =
|
||||
Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher();
|
||||
|
||||
// Construct basename matcher.
|
||||
let basename_matcher = Glob::new(&per_file_ignore.basename)?.compile_matcher();
|
||||
|
||||
Ok(CompiledPerFileIgnore {
|
||||
absolute_matcher,
|
||||
basename_matcher,
|
||||
negated: per_file_ignore.negated,
|
||||
rules: per_file_ignore.rules,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(Self { ignores: ignores? })
|
||||
impl<T> CacheKey for CompiledPerFileList<T>
|
||||
where
|
||||
T: CacheKey,
|
||||
{
|
||||
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||
self.inner.cache_key(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CompiledPerFileIgnoreList {
|
||||
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()
|
||||
.map(|per_file_ignore| {
|
||||
// Construct absolute path matcher.
|
||||
let absolute_matcher = Glob::new(&per_file_ignore.absolute.to_string_lossy())
|
||||
.with_context(|| format!("invalid glob {:?}", per_file_ignore.absolute))?
|
||||
.compile_matcher();
|
||||
|
||||
// Construct basename matcher.
|
||||
let basename_matcher = Glob::new(&per_file_ignore.basename)
|
||||
.with_context(|| format!("invalid glob {:?}", per_file_ignore.basename))?
|
||||
.compile_matcher();
|
||||
|
||||
Ok(CompiledPerFile::new(
|
||||
absolute_matcher,
|
||||
basename_matcher,
|
||||
per_file_ignore.negated,
|
||||
per_file_ignore.data,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
Ok(Self { inner: inner? })
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<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 {
|
||||
if self.ignores.is_empty() {
|
||||
if self.inner.is_empty() {
|
||||
write!(f, "{{}}")?;
|
||||
} else {
|
||||
writeln!(f, "{{")?;
|
||||
for ignore in &self.ignores {
|
||||
writeln!(f, "\t{ignore}")?;
|
||||
for value in &self.inner {
|
||||
writeln!(f, "\t{value}")?;
|
||||
}
|
||||
write!(f, "}}")?;
|
||||
}
|
||||
|
@ -627,11 +756,70 @@ impl Display for CompiledPerFileIgnoreList {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, CacheKey, Default)]
|
||||
pub struct CompiledPerFileIgnoreList(CompiledPerFileList<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 {
|
||||
type Target = Vec<CompiledPerFileIgnore>;
|
||||
type Target = CompiledPerFileList<RuleSet>;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::path::Path;
|
||||
|
||||
use ruff_formatter::PrintedRange;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError};
|
||||
|
@ -10,8 +12,10 @@ pub(crate) fn format(
|
|||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
path: Option<&Path>,
|
||||
) -> crate::Result<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) {
|
||||
Ok(formatted) => {
|
||||
let formatted = formatted.into_code();
|
||||
|
@ -36,8 +40,10 @@ pub(crate) fn format_range(
|
|||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
range: TextRange,
|
||||
path: Option<&Path>,
|
||||
) -> crate::Result<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) {
|
||||
Ok(formatted) => {
|
||||
|
@ -56,3 +62,146 @@ pub(crate) fn format_range(
|
|||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion};
|
||||
use ruff_python_ast::{PySourceType, PythonVersion};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::format::{format, format_range};
|
||||
use crate::TextDocument;
|
||||
|
||||
#[test]
|
||||
fn format_per_file_version() {
|
||||
let document = TextDocument::new(r#"
|
||||
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
|
||||
pass
|
||||
"#.to_string(), 0);
|
||||
let per_file_target_version =
|
||||
CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
|
||||
"test.py".to_string(),
|
||||
PythonVersion::PY310,
|
||||
Some(Path::new(".")),
|
||||
)])
|
||||
.unwrap();
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&FormatterSettings {
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
per_file_target_version,
|
||||
..Default::default()
|
||||
},
|
||||
Some(Path::new("test.py")),
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
assert_snapshot!(result, @r#"
|
||||
with (
|
||||
open("a_really_long_foo") as foo,
|
||||
open("a_really_long_bar") as bar,
|
||||
open("a_really_long_baz") as baz,
|
||||
):
|
||||
pass
|
||||
"#);
|
||||
|
||||
// same as above but without the per_file_target_version override
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&FormatterSettings {
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
..Default::default()
|
||||
},
|
||||
Some(Path::new("test.py")),
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
assert_snapshot!(result, @r#"
|
||||
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
|
||||
"a_really_long_baz"
|
||||
) as baz:
|
||||
pass
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_per_file_version_range() -> anyhow::Result<()> {
|
||||
// prepare a document with formatting changes before and after the intended range (the
|
||||
// context manager)
|
||||
let document = TextDocument::new(r#"
|
||||
def fn(x: str) -> Foo | Bar: return foobar(x)
|
||||
|
||||
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
|
||||
pass
|
||||
|
||||
sys.exit(
|
||||
1
|
||||
)
|
||||
"#.to_string(), 0);
|
||||
|
||||
let start = document.contents().find("with").unwrap();
|
||||
let end = document.contents().find("pass").unwrap() + "pass".len();
|
||||
let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);
|
||||
|
||||
let per_file_target_version =
|
||||
CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
|
||||
"test.py".to_string(),
|
||||
PythonVersion::PY310,
|
||||
Some(Path::new(".")),
|
||||
)])
|
||||
.unwrap();
|
||||
let result = format_range(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&FormatterSettings {
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
per_file_target_version,
|
||||
..Default::default()
|
||||
},
|
||||
range,
|
||||
Some(Path::new("test.py")),
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
assert_snapshot!(result.as_code(), @r#"
|
||||
with (
|
||||
open("a_really_long_foo") as foo,
|
||||
open("a_really_long_bar") as bar,
|
||||
open("a_really_long_baz") as baz,
|
||||
):
|
||||
pass
|
||||
"#);
|
||||
|
||||
// same as above but without the per_file_target_version override
|
||||
let result = format_range(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&FormatterSettings {
|
||||
unresolved_target_version: PythonVersion::PY38,
|
||||
..Default::default()
|
||||
},
|
||||
range,
|
||||
Some(Path::new("test.py")),
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
assert_snapshot!(result.as_code(), @r#"
|
||||
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
|
||||
"a_really_long_baz"
|
||||
) as baz:
|
||||
pass
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,9 +85,10 @@ fn format_text_document(
|
|||
let settings = query.settings();
|
||||
|
||||
// If the document is excluded, return early.
|
||||
if let Some(file_path) = query.file_path() {
|
||||
let file_path = query.file_path();
|
||||
if let Some(file_path) = &file_path {
|
||||
if is_document_excluded_for_formatting(
|
||||
&file_path,
|
||||
file_path,
|
||||
&settings.file_resolver,
|
||||
&settings.formatter,
|
||||
text_document.language_id(),
|
||||
|
@ -97,8 +98,13 @@ fn format_text_document(
|
|||
}
|
||||
|
||||
let source = text_document.contents();
|
||||
let formatted = crate::format::format(text_document, query.source_type(), &settings.formatter)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
let formatted = crate::format::format(
|
||||
text_document,
|
||||
query.source_type(),
|
||||
&settings.formatter,
|
||||
file_path.as_deref(),
|
||||
)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
let Some(mut formatted) = formatted else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
|
|
@ -49,9 +49,10 @@ fn format_text_document_range(
|
|||
let settings = query.settings();
|
||||
|
||||
// If the document is excluded, return early.
|
||||
if let Some(file_path) = query.file_path() {
|
||||
let file_path = query.file_path();
|
||||
if let Some(file_path) = &file_path {
|
||||
if is_document_excluded_for_formatting(
|
||||
&file_path,
|
||||
file_path,
|
||||
&settings.file_resolver,
|
||||
&settings.formatter,
|
||||
text_document.language_id(),
|
||||
|
@ -68,6 +69,7 @@ fn format_text_document_range(
|
|||
query.source_type(),
|
||||
&settings.formatter,
|
||||
range,
|
||||
file_path.as_deref(),
|
||||
)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
|
||||
|
|
|
@ -303,7 +303,7 @@ impl<'a> ParsedModule<'a> {
|
|||
// TODO(konstin): Add an options for py/pyi to the UI (2/2)
|
||||
let options = settings
|
||||
.formatter
|
||||
.to_format_options(PySourceType::default(), self.source_code)
|
||||
.to_format_options(PySourceType::default(), self.source_code, None)
|
||||
.with_source_map_generation(SourceMapGeneration::Enabled);
|
||||
|
||||
format_module_ast(
|
||||
|
|
|
@ -9,7 +9,7 @@ use std::num::{NonZeroU16, NonZeroU8};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use glob::{glob, GlobError, Paths, PatternError};
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
|
@ -29,8 +29,9 @@ use ruff_linter::rules::{flake8_import_conventions, isort, pycodestyle};
|
|||
use ruff_linter::settings::fix_safety_table::FixSafetyTable;
|
||||
use ruff_linter::settings::rule_table::RuleTable;
|
||||
use ruff_linter::settings::types::{
|
||||
CompiledPerFileIgnoreList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat,
|
||||
PerFileIgnore, PreviewMode, RequiredVersion, UnsafeFixes,
|
||||
CompiledPerFileIgnoreList, CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern,
|
||||
FilePatternSet, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode,
|
||||
RequiredVersion, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
|
||||
use ruff_linter::{
|
||||
|
@ -138,6 +139,7 @@ pub struct Configuration {
|
|||
pub namespace_packages: Option<Vec<PathBuf>>,
|
||||
pub src: Option<Vec<PathBuf>>,
|
||||
pub target_version: Option<ast::PythonVersion>,
|
||||
pub per_file_target_version: Option<Vec<PerFileTargetVersion>>,
|
||||
|
||||
// Global formatting options
|
||||
pub line_length: Option<LineLength>,
|
||||
|
@ -174,11 +176,17 @@ impl Configuration {
|
|||
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
|
||||
};
|
||||
|
||||
let per_file_target_version = CompiledPerFileTargetVersionList::resolve(
|
||||
self.per_file_target_version.unwrap_or_default(),
|
||||
)
|
||||
.context("failed to resolve `per-file-target-version` table")?;
|
||||
|
||||
let formatter = FormatterSettings {
|
||||
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
|
||||
extension: self.extension.clone().unwrap_or_default(),
|
||||
preview: format_preview,
|
||||
target_version,
|
||||
unresolved_target_version: target_version,
|
||||
per_file_target_version: per_file_target_version.clone(),
|
||||
line_width: self
|
||||
.line_length
|
||||
.map_or(format_defaults.line_width, |length| {
|
||||
|
@ -278,7 +286,8 @@ impl Configuration {
|
|||
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
|
||||
extension: self.extension.unwrap_or_default(),
|
||||
preview: lint_preview,
|
||||
target_version,
|
||||
unresolved_target_version: target_version,
|
||||
per_file_target_version,
|
||||
project_root: project_root.to_path_buf(),
|
||||
allowed_confusables: lint
|
||||
.allowed_confusables
|
||||
|
@ -533,6 +542,18 @@ impl Configuration {
|
|||
.map(|src| resolve_src(&src, project_root))
|
||||
.transpose()?,
|
||||
target_version: options.target_version.map(ast::PythonVersion::from),
|
||||
per_file_target_version: options.per_file_target_version.map(|versions| {
|
||||
versions
|
||||
.into_iter()
|
||||
.map(|(pattern, version)| {
|
||||
PerFileTargetVersion::new(
|
||||
pattern,
|
||||
ast::PythonVersion::from(version),
|
||||
Some(project_root),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
// `--extension` is a hidden command-line argument that isn't supported in configuration
|
||||
// files at present.
|
||||
extension: None,
|
||||
|
@ -580,6 +601,9 @@ impl Configuration {
|
|||
show_fixes: self.show_fixes.or(config.show_fixes),
|
||||
src: self.src.or(config.src),
|
||||
target_version: self.target_version.or(config.target_version),
|
||||
per_file_target_version: self
|
||||
.per_file_target_version
|
||||
.or(config.per_file_target_version),
|
||||
preview: self.preview.or(config.preview),
|
||||
extension: self.extension.or(config.extension),
|
||||
|
||||
|
|
|
@ -333,6 +333,29 @@ pub struct Options {
|
|||
)]
|
||||
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
|
||||
/// imports.
|
||||
///
|
||||
|
|
|
@ -4,11 +4,12 @@ use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
|
|||
use ruff_graph::AnalyzeSettings;
|
||||
use ruff_linter::display_settings;
|
||||
use ruff_linter::settings::types::{
|
||||
ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, UnsafeFixes,
|
||||
CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat,
|
||||
UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::settings::LinterSettings;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_ast::{PySourceType, PythonVersion};
|
||||
use ruff_python_formatter::{
|
||||
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions,
|
||||
QuoteStyle,
|
||||
|
@ -164,7 +165,17 @@ pub struct FormatterSettings {
|
|||
pub exclude: FilePatternSet,
|
||||
pub extension: ExtensionMapping,
|
||||
pub preview: PreviewMode,
|
||||
pub target_version: ruff_python_ast::PythonVersion,
|
||||
/// The non-path-resolved Python version specified by the `target-version` input option.
|
||||
///
|
||||
/// See [`FormatterSettings::resolve_target_version`] for a way to obtain the Python version for
|
||||
/// a given file, while respecting the overrides in `per_file_target_version`.
|
||||
pub unresolved_target_version: PythonVersion,
|
||||
/// Path-specific overrides to `unresolved_target_version`.
|
||||
///
|
||||
/// See [`FormatterSettings::resolve_target_version`] for a way to check a given [`Path`]
|
||||
/// against these patterns, while falling back to `unresolved_target_version` if none of them
|
||||
/// match.
|
||||
pub per_file_target_version: CompiledPerFileTargetVersionList,
|
||||
|
||||
pub line_width: LineWidth,
|
||||
|
||||
|
@ -182,7 +193,16 @@ pub struct FormatterSettings {
|
|||
}
|
||||
|
||||
impl FormatterSettings {
|
||||
pub fn to_format_options(&self, source_type: PySourceType, source: &str) -> PyFormatOptions {
|
||||
pub fn to_format_options(
|
||||
&self,
|
||||
source_type: PySourceType,
|
||||
source: &str,
|
||||
path: Option<&Path>,
|
||||
) -> PyFormatOptions {
|
||||
let target_version = path
|
||||
.map(|path| self.resolve_target_version(path))
|
||||
.unwrap_or(self.unresolved_target_version);
|
||||
|
||||
let line_ending = match self.line_ending {
|
||||
LineEnding::Lf => ruff_formatter::printer::LineEnding::LineFeed,
|
||||
LineEnding::CrLf => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
|
||||
|
@ -205,7 +225,7 @@ impl FormatterSettings {
|
|||
};
|
||||
|
||||
PyFormatOptions::from_source_type(source_type)
|
||||
.with_target_version(self.target_version)
|
||||
.with_target_version(target_version)
|
||||
.with_indent_style(self.indent_style)
|
||||
.with_indent_width(self.indent_width)
|
||||
.with_quote_style(self.quote_style)
|
||||
|
@ -216,6 +236,17 @@ impl FormatterSettings {
|
|||
.with_docstring_code(self.docstring_code_format)
|
||||
.with_docstring_code_line_width(self.docstring_code_line_width)
|
||||
}
|
||||
|
||||
/// Resolve the [`PythonVersion`] to use for formatting.
|
||||
///
|
||||
/// This method respects the per-file version overrides in
|
||||
/// [`FormatterSettings::per_file_target_version`] and falls back on
|
||||
/// [`FormatterSettings::unresolved_target_version`] if none of the override patterns match.
|
||||
pub fn resolve_target_version(&self, path: &Path) -> PythonVersion {
|
||||
self.per_file_target_version
|
||||
.is_match(path)
|
||||
.unwrap_or(self.unresolved_target_version)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FormatterSettings {
|
||||
|
@ -225,7 +256,8 @@ impl Default for FormatterSettings {
|
|||
Self {
|
||||
exclude: FilePatternSet::default(),
|
||||
extension: ExtensionMapping::default(),
|
||||
target_version: default_options.target_version(),
|
||||
unresolved_target_version: default_options.target_version(),
|
||||
per_file_target_version: CompiledPerFileTargetVersionList::default(),
|
||||
preview: PreviewMode::Disabled,
|
||||
line_width: default_options.line_width(),
|
||||
line_ending: LineEnding::Auto,
|
||||
|
@ -247,7 +279,8 @@ impl fmt::Display for FormatterSettings {
|
|||
namespace = "formatter",
|
||||
fields = [
|
||||
self.exclude,
|
||||
self.target_version,
|
||||
self.unresolved_target_version,
|
||||
self.per_file_target_version,
|
||||
self.preview,
|
||||
self.line_width,
|
||||
self.line_ending,
|
||||
|
|
10
ruff.schema.json
generated
10
ruff.schema.json
generated
|
@ -578,6 +578,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"per-file-target-version": {
|
||||
"description": "A list of mappings from glob-style file pattern to Python version to use when checking the corresponding file(s).\n\nThis may be useful for overriding the global Python version settings in `target-version` or `requires-python` for a subset of files. For example, if you have a project with a minimum supported Python version of 3.9 but a subdirectory of developer scripts that want to use a newer feature like the `match` statement from Python 3.10, you can use `per-file-target-version` to specify `\"developer_scripts/*.py\" = \"py310\"`.\n\nThis setting is used by the linter to enforce any enabled version-specific lint rules, as well as by the formatter for any version-specific formatting options, such as parenthesizing context managers on Python 3.10+.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PythonVersion"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"description": "Whether to enable preview mode. When preview mode is enabled, Ruff will use unstable rules, fixes, and formatting.",
|
||||
"type": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue