From 3c9f5e2fdcce32d56515da1063e0fd23e5d7c215 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 27 Apr 2023 00:02:17 -0400 Subject: [PATCH 01/32] Preserve star-handling special-casing for force-single-line (#4129) --- .../test/fixtures/isort/force_single_line.py | 3 ++ crates/ruff/src/rules/isort/normalize.rs | 3 +- ...orce_single_line_force_single_line.py.snap | 49 +++++++++++-------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/isort/force_single_line.py b/crates/ruff/resources/test/fixtures/isort/force_single_line.py index 4dc242baa6a1a..673ff854d338e 100644 --- a/crates/ruff/resources/test/fixtures/isort/force_single_line.py +++ b/crates/ruff/resources/test/fixtures/isort/force_single_line.py @@ -22,3 +22,6 @@ a, # comment 7 b, # comment 8 ) + +# comment 9 +from baz import * # comment 10 diff --git a/crates/ruff/src/rules/isort/normalize.rs b/crates/ruff/src/rules/isort/normalize.rs index 0988b4b0866cf..bce474247a9b6 100644 --- a/crates/ruff/src/rules/isort/normalize.rs +++ b/crates/ruff/src/rules/isort/normalize.rs @@ -56,7 +56,8 @@ pub fn normalize_imports<'a>( } => { // Whether to track each member of the import as a separate entry. let isolate_aliases = force_single_line - && module.map_or(true, |module| !single_line_exclusions.contains(module)); + && module.map_or(true, |module| !single_line_exclusions.contains(module)) + && !names.first().map_or(false, |alias| alias.name == "*"); // Insert comments on the statement itself. if isolate_aliases { diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__force_single_line_force_single_line.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__force_single_line_force_single_line.py.snap index 07fdd0d9c8b66..1d042dfcf84f8 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__force_single_line_force_single_line.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__force_single_line_force_single_line.py.snap @@ -27,6 +27,9 @@ force_single_line.py:1:1: I001 [*] Import block is un-sorted or un-formatted 22 | | a, # comment 7 23 | | b, # comment 8 24 | | ) +25 | | +26 | | # comment 9 +27 | | from baz import * # comment 10 | = help: Organize imports @@ -44,35 +47,39 @@ force_single_line.py:1:1: I001 [*] Import block is un-sorted or un-formatted 8 |+from logging.handlers import FileHandler, StreamHandler 9 |+from os import path, uname 9 10 | +10 |-# comment 1 +11 |-from third_party import lib1, lib2, \ +12 |- lib3, lib7, lib5, lib6 +13 |-# comment 2 +14 |-from third_party import lib4 11 |+# comment 6 12 |+from bar import a # comment 7 13 |+from bar import b # comment 8 - 14 |+from foo import bar # comment 3 - 15 |+from foo2 import bar2 # comment 4 - 16 |+from foo3 import bar3 # comment 5 - 17 |+from foo3 import baz3 # comment 5 - 18 |+ -10 19 | # comment 1 -11 |-from third_party import lib1, lib2, \ -12 |- lib3, lib7, lib5, lib6 - 20 |+from third_party import lib1 - 21 |+from third_party import lib2 - 22 |+from third_party import lib3 - 23 |+ -13 24 | # comment 2 -14 25 | from third_party import lib4 -15 |- -16 |-from foo import bar # comment 3 -17 |-from foo2 import bar2 # comment 4 +15 14 | + 15 |+# comment 9 + 16 |+from baz import * # comment 10 +16 17 | from foo import bar # comment 3 +17 18 | from foo2 import bar2 # comment 4 18 |-from foo3 import bar3, baz3 # comment 5 -19 |- + 19 |+from foo3 import bar3 # comment 5 + 20 |+from foo3 import baz3 # comment 5 +19 21 | 20 |-# comment 6 21 |-from bar import ( 22 |- a, # comment 7 23 |- b, # comment 8 24 |-) - 26 |+from third_party import lib5 - 27 |+from third_party import lib6 - 28 |+from third_party import lib7 + 22 |+# comment 1 + 23 |+from third_party import lib1 + 24 |+from third_party import lib2 + 25 |+from third_party import lib3 +25 26 | +26 |-# comment 9 +27 |-from baz import * # comment 10 + 27 |+# comment 2 + 28 |+from third_party import lib4 + 29 |+from third_party import lib5 + 30 |+from third_party import lib6 + 31 |+from third_party import lib7 From 3e81403fbe6f3897a3bee2591d928028315eae6c Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Thu, 27 Apr 2023 19:33:07 +0100 Subject: [PATCH 02/32] Add pygrep-hooks documentation (#4131) --- _typos.toml | 1 + .../rules/pygrep_hooks/rules/blanket_noqa.rs | 23 +++++++++++++++++ .../pygrep_hooks/rules/blanket_type_ignore.rs | 23 +++++++++++++++++ .../pygrep_hooks/rules/deprecated_log_warn.rs | 25 +++++++++++++++++++ .../src/rules/pygrep_hooks/rules/no_eval.rs | 23 +++++++++++++++++ 5 files changed, 95 insertions(+) diff --git a/_typos.toml b/_typos.toml index a14ac7083f336..c0c02513b57db 100644 --- a/_typos.toml +++ b/_typos.toml @@ -6,3 +6,4 @@ trivias = "trivias" hel = "hel" whos = "whos" spawnve = "spawnve" +ned = "ned" diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs index 82a77fe523e23..23673721fee3d 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_noqa.rs @@ -6,6 +6,29 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::newlines::Line; +/// ## What it does +/// Check for `noqa` annotations that suppress all diagnostics, as opposed to +/// targeting specific diagnostics. +/// +/// ## Why is this bad? +/// Suppressing all diagnostics can hide issues in the code. +/// +/// Blanket `noqa` annotations are also more difficult to interpret and +/// maintain, as the annotation does not clarify which diagnostics are intended +/// to be suppressed. +/// +/// ## Example +/// ```python +/// from .base import * # noqa +/// ``` +/// +/// Use instead: +/// ```python +/// from .base import * # noqa: F403 +/// ``` +/// +/// ## References +/// - [Ruff documentation](https://beta.ruff.rs/docs/configuration/#error-suppression) #[violation] pub struct BlanketNOQA; diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs index 629d1f8db60bf..d16c9e7c226b1 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs @@ -7,6 +7,29 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::newlines::Line; +/// ## What it does +/// Check for `type: ignore` annotations that suppress all type warnings, as +/// opposed to targeting specific type warnings. +/// +/// ## Why is this bad? +/// Suppressing all warnings can hide issues in the code. +/// +/// Blanket `type: ignore` annotations are also more difficult to interpret and +/// maintain, as the annotation does not clarify which warnings are intended +/// to be suppressed. +/// +/// ## Example +/// ```python +/// from foo import secrets # type: ignore +/// ``` +/// +/// Use instead: +/// ```python +/// from foo import secrets # type: ignore[attr-defined] +/// ``` +/// +/// ## References +/// - [mypy](https://mypy.readthedocs.io/en/stable/common_issues.html#spurious-errors-and-locally-silencing-the-checker) #[violation] pub struct BlanketTypeIgnore; diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs index 5d83e83e321d4..769898a3719c1 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs @@ -5,6 +5,31 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Check for usages of the deprecated `warn` method from the `logging` module. +/// +/// ## Why is this bad? +/// The `warn` method is deprecated. Use `warning` instead. +/// +/// ## Example +/// ```python +/// import logging +/// +/// +/// def foo(): +/// logging.warn("Something happened") +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// def foo(): +/// logging.warning("Something happened") +/// ``` +/// +/// ## References +/// - [Python documentation](https://docs.python.org/3/library/logging.html#logging.Logger.warning) #[violation] pub struct DeprecatedLogWarn; diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs index e9cf461e7af48..df0cc8d34ca57 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs @@ -5,6 +5,29 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for usages of the builtin `eval()` function. +/// +/// ## Why is this bad? +/// The `eval()` function is insecure as it enables arbitrary code execution. +/// +/// ## Example +/// ```python +/// def foo(): +/// x = eval(input("Enter a number: ")) +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(): +/// x = input("Enter a number: ") +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation](https://docs.python.org/3/library/functions.html#eval) +/// - [_Eval really is dangerous_ by Ned Batchelder](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html) #[violation] pub struct Eval; From 089b64e9c1cd46931123a0cfa302eb62ed35acff Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 28 Apr 2023 00:23:27 +0530 Subject: [PATCH 03/32] Autofix `EM101`, `EM102`, `EM103` if possible (#4123) --- .../test/fixtures/flake8_errmsg/EM.py | 33 ++++ crates/ruff/src/checkers/ast/mod.rs | 2 +- crates/ruff/src/rules/flake8_errmsg/rules.rs | 165 +++++++++++++++-- ...__rules__flake8_errmsg__tests__custom.snap | 156 +++++++++++++++- ...rules__flake8_errmsg__tests__defaults.snap | 170 +++++++++++++++++- 5 files changed, 502 insertions(+), 24 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_errmsg/EM.py b/crates/ruff/resources/test/fixtures/flake8_errmsg/EM.py index 144722a60ac6a..1311bbc675f74 100644 --- a/crates/ruff/resources/test/fixtures/flake8_errmsg/EM.py +++ b/crates/ruff/resources/test/fixtures/flake8_errmsg/EM.py @@ -21,3 +21,36 @@ def f_c(): def f_ok(): msg = "hello" raise RuntimeError(msg) + + +def f_unfixable(): + msg = "hello" + raise RuntimeError("This is an example exception") + + +def f_msg_in_nested_scope(): + def nested(): + msg = "hello" + + raise RuntimeError("This is an example exception") + + +def f_msg_in_parent_scope(): + msg = "hello" + + def nested(): + raise RuntimeError("This is an example exception") + + +def f_fix_indentation_check(foo): + if foo: + raise RuntimeError("This is an example exception") + else: + if foo == "foo": + raise RuntimeError(f"This is an exception: {foo}") + raise RuntimeError("This is an exception: {}".format(foo)) + + +# Report these, but don't fix them +if foo: raise RuntimeError("This is an example exception") +if foo: x = 1; raise RuntimeError("This is an example exception") diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index b3f850d67dde9..0a20f2a5669b5 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1512,7 +1512,7 @@ where Rule::DotFormatInException, ]) { if let Some(exc) = exc { - flake8_errmsg::rules::string_in_exception(self, exc); + flake8_errmsg::rules::string_in_exception(self, stmt, exc); } } if self.settings.rules.enabled(Rule::OSErrorAlias) { diff --git a/crates/ruff/src/rules/flake8_errmsg/rules.rs b/crates/ruff/src/rules/flake8_errmsg/rules.rs index 0a1d16a710d59..ac30f0d7c02cf 100644 --- a/crates/ruff/src/rules/flake8_errmsg/rules.rs +++ b/crates/ruff/src/rules/flake8_errmsg/rules.rs @@ -1,10 +1,13 @@ -use rustpython_parser::ast::{Constant, Expr, ExprKind}; +use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind}; -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::{create_expr, create_stmt, unparse_stmt}; +use ruff_python_ast::source_code::Stylist; +use ruff_python_ast::whitespace; use crate::checkers::ast::Checker; -use crate::registry::Rule; +use crate::registry::{AsRule, Rule}; /// ## What it does /// Checks for the use of string literals in exception constructors. @@ -44,13 +47,22 @@ use crate::registry::Rule; /// RuntimeError: 'Some value' is incorrect /// ``` #[violation] -pub struct RawStringInException; +pub struct RawStringInException { + pub fixable: bool, +} impl Violation for RawStringInException { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Exception must not use a string literal, assign to variable first") } + + fn autofix_title_formatter(&self) -> Option String> { + self.fixable + .then_some(|_| format!("Assign to variable; remove string literal")) + } } /// ## What it does @@ -92,13 +104,22 @@ impl Violation for RawStringInException { /// RuntimeError: 'Some value' is incorrect /// ``` #[violation] -pub struct FStringInException; +pub struct FStringInException { + pub fixable: bool, +} impl Violation for FStringInException { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Exception must not use an f-string literal, assign to variable first") } + + fn autofix_title_formatter(&self) -> Option String> { + self.fixable + .then_some(|_| format!("Assign to variable; remove f-string literal")) + } } /// ## What it does @@ -142,17 +163,62 @@ impl Violation for FStringInException { /// RuntimeError: 'Some value' is incorrect /// ``` #[violation] -pub struct DotFormatInException; +pub struct DotFormatInException { + pub fixable: bool, +} impl Violation for DotFormatInException { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Exception must not use a `.format()` string directly, assign to variable first") } + + fn autofix_title_formatter(&self) -> Option String> { + self.fixable + .then_some(|_| format!("Assign to variable; remove `.format()` string")) + } +} + +/// Generate the [`Fix`] for EM001, EM002, and EM003 violations. +/// +/// This assumes that the violation is fixable and that the patch should +/// be generated. The exception argument should be either a string literal, +/// an f-string, or a `.format` string. +/// +/// The fix includes two edits: +/// 1. Insert the exception argument into a variable assignment before the +/// `raise` statement. The variable name is `msg`. +/// 2. Replace the exception argument with the variable name. +fn generate_fix(stylist: &Stylist, stmt: &Stmt, exc_arg: &Expr, indentation: &str) -> Fix { + let assignment = unparse_stmt( + &create_stmt(StmtKind::Assign { + targets: vec![create_expr(ExprKind::Name { + id: String::from("msg"), + ctx: ExprContext::Store, + })], + value: Box::new(exc_arg.clone()), + type_comment: None, + }), + stylist, + ); + Fix::from_iter([ + Edit::insertion( + format!( + "{}{}{}", + assignment, + stylist.line_ending().as_str(), + indentation, + ), + stmt.start(), + ), + Edit::range_replacement(String::from("msg"), exc_arg.range()), + ]) } /// EM101, EM102, EM103 -pub fn string_in_exception(checker: &mut Checker, exc: &Expr) { +pub fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr) { if let ExprKind::Call { args, .. } = &exc.node { if let Some(first) = args.first() { match &first.node { @@ -163,18 +229,63 @@ pub fn string_in_exception(checker: &mut Checker, exc: &Expr) { } => { if checker.settings.rules.enabled(Rule::RawStringInException) { if string.len() > checker.settings.flake8_errmsg.max_string_length { - checker - .diagnostics - .push(Diagnostic::new(RawStringInException, first.range())); + let indentation = whitespace::indentation(checker.locator, stmt) + .and_then(|indentation| { + if checker.ctx.find_binding("msg").is_none() { + Some(indentation) + } else { + None + } + }); + let mut diagnostic = Diagnostic::new( + RawStringInException { + fixable: indentation.is_some(), + }, + first.range(), + ); + if let Some(indentation) = indentation { + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(generate_fix( + checker.stylist, + stmt, + first, + indentation, + )); + } + } + checker.diagnostics.push(diagnostic); } } } // Check for f-strings ExprKind::JoinedStr { .. } => { if checker.settings.rules.enabled(Rule::FStringInException) { - checker - .diagnostics - .push(Diagnostic::new(FStringInException, first.range())); + let indentation = whitespace::indentation(checker.locator, stmt).and_then( + |indentation| { + if checker.ctx.find_binding("msg").is_none() { + Some(indentation) + } else { + None + } + }, + ); + let mut diagnostic = Diagnostic::new( + FStringInException { + fixable: indentation.is_some(), + }, + first.range(), + ); + if let Some(indentation) = indentation { + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(generate_fix( + checker.stylist, + stmt, + first, + indentation, + )); + } + } + checker.diagnostics.push(diagnostic); } } // Check for .format() calls @@ -182,9 +293,31 @@ pub fn string_in_exception(checker: &mut Checker, exc: &Expr) { if checker.settings.rules.enabled(Rule::DotFormatInException) { if let ExprKind::Attribute { value, attr, .. } = &func.node { if attr == "format" && matches!(value.node, ExprKind::Constant { .. }) { - checker - .diagnostics - .push(Diagnostic::new(DotFormatInException, first.range())); + let indentation = whitespace::indentation(checker.locator, stmt) + .and_then(|indentation| { + if checker.ctx.find_binding("msg").is_none() { + Some(indentation) + } else { + None + } + }); + let mut diagnostic = Diagnostic::new( + DotFormatInException { + fixable: indentation.is_some(), + }, + first.range(), + ); + if let Some(indentation) = indentation { + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(generate_fix( + checker.stylist, + stmt, + first, + indentation, + )); + } + } + checker.diagnostics.push(diagnostic); } } } diff --git a/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__custom.snap b/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__custom.snap index 9768f244bdb44..73074676c1a24 100644 --- a/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__custom.snap +++ b/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__custom.snap @@ -1,26 +1,176 @@ --- source: crates/ruff/src/rules/flake8_errmsg/mod.rs --- -EM.py:5:24: EM101 Exception must not use a string literal, assign to variable first +EM.py:5:24: EM101 [*] Exception must not use a string literal, assign to variable first | 5 | def f_a(): 6 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 | + = help: Assign to variable; remove string literal -EM.py:14:24: EM102 Exception must not use an f-string literal, assign to variable first +ℹ Suggested fix +2 2 | +3 3 | +4 4 | def f_a(): +5 |- raise RuntimeError("This is an example exception") + 5 |+ msg = "This is an example exception" + 6 |+ raise RuntimeError(msg) +6 7 | +7 8 | +8 9 | def f_a_short(): + +EM.py:14:24: EM102 [*] Exception must not use an f-string literal, assign to variable first | 14 | def f_b(): 15 | example = "example" 16 | raise RuntimeError(f"This is an {example} exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM102 | + = help: Assign to variable; remove f-string literal + +ℹ Suggested fix +11 11 | +12 12 | def f_b(): +13 13 | example = "example" +14 |- raise RuntimeError(f"This is an {example} exception") + 14 |+ msg = f"This is an {example} exception" + 15 |+ raise RuntimeError(msg) +15 16 | +16 17 | +17 18 | def f_c(): -EM.py:18:24: EM103 Exception must not use a `.format()` string directly, assign to variable first +EM.py:18:24: EM103 [*] Exception must not use a `.format()` string directly, assign to variable first | 18 | def f_c(): 19 | raise RuntimeError("This is an {example} exception".format(example="example")) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM103 | + = help: Assign to variable; remove `.format()` string + +ℹ Suggested fix +15 15 | +16 16 | +17 17 | def f_c(): +18 |- raise RuntimeError("This is an {example} exception".format(example="example")) + 18 |+ msg = "This is an {example} exception".format(example="example") + 19 |+ raise RuntimeError(msg) +19 20 | +20 21 | +21 22 | def f_ok(): + +EM.py:28:24: EM101 Exception must not use a string literal, assign to variable first + | +28 | def f_unfixable(): +29 | msg = "hello" +30 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | + +EM.py:35:24: EM101 [*] Exception must not use a string literal, assign to variable first + | +35 | msg = "hello" +36 | +37 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | + = help: Assign to variable; remove string literal + +ℹ Suggested fix +32 32 | def nested(): +33 33 | msg = "hello" +34 34 | +35 |- raise RuntimeError("This is an example exception") + 35 |+ msg = "This is an example exception" + 36 |+ raise RuntimeError(msg) +36 37 | +37 38 | +38 39 | def f_msg_in_parent_scope(): + +EM.py:42:28: EM101 Exception must not use a string literal, assign to variable first + | +42 | def nested(): +43 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | + +EM.py:47:28: EM101 [*] Exception must not use a string literal, assign to variable first + | +47 | def f_fix_indentation_check(foo): +48 | if foo: +49 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 +50 | else: +51 | if foo == "foo": + | + = help: Assign to variable; remove string literal + +ℹ Suggested fix +44 44 | +45 45 | def f_fix_indentation_check(foo): +46 46 | if foo: +47 |- raise RuntimeError("This is an example exception") + 47 |+ msg = "This is an example exception" + 48 |+ raise RuntimeError(msg) +48 49 | else: +49 50 | if foo == "foo": +50 51 | raise RuntimeError(f"This is an exception: {foo}") + +EM.py:50:32: EM102 [*] Exception must not use an f-string literal, assign to variable first + | +50 | else: +51 | if foo == "foo": +52 | raise RuntimeError(f"This is an exception: {foo}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM102 +53 | raise RuntimeError("This is an exception: {}".format(foo)) + | + = help: Assign to variable; remove f-string literal + +ℹ Suggested fix +47 47 | raise RuntimeError("This is an example exception") +48 48 | else: +49 49 | if foo == "foo": +50 |- raise RuntimeError(f"This is an exception: {foo}") + 50 |+ msg = f"This is an exception: {foo}" + 51 |+ raise RuntimeError(msg) +51 52 | raise RuntimeError("This is an exception: {}".format(foo)) +52 53 | +53 54 | + +EM.py:51:24: EM103 [*] Exception must not use a `.format()` string directly, assign to variable first + | +51 | if foo == "foo": +52 | raise RuntimeError(f"This is an exception: {foo}") +53 | raise RuntimeError("This is an exception: {}".format(foo)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM103 + | + = help: Assign to variable; remove `.format()` string + +ℹ Suggested fix +48 48 | else: +49 49 | if foo == "foo": +50 50 | raise RuntimeError(f"This is an exception: {foo}") +51 |- raise RuntimeError("This is an exception: {}".format(foo)) + 51 |+ msg = "This is an exception: {}".format(foo) + 52 |+ raise RuntimeError(msg) +52 53 | +53 54 | +54 55 | # Report these, but don't fix them + +EM.py:55:28: EM101 Exception must not use a string literal, assign to variable first + | +55 | # Report these, but don't fix them +56 | if foo: raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 +57 | if foo: x = 1; raise RuntimeError("This is an example exception") + | + +EM.py:56:35: EM101 Exception must not use a string literal, assign to variable first + | +56 | # Report these, but don't fix them +57 | if foo: raise RuntimeError("This is an example exception") +58 | if foo: x = 1; raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | diff --git a/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__defaults.snap b/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__defaults.snap index b1adb139c5565..87af51141d3eb 100644 --- a/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_errmsg/snapshots/ruff__rules__flake8_errmsg__tests__defaults.snap @@ -1,33 +1,195 @@ --- source: crates/ruff/src/rules/flake8_errmsg/mod.rs --- -EM.py:5:24: EM101 Exception must not use a string literal, assign to variable first +EM.py:5:24: EM101 [*] Exception must not use a string literal, assign to variable first | 5 | def f_a(): 6 | raise RuntimeError("This is an example exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 | + = help: Assign to variable; remove string literal -EM.py:9:24: EM101 Exception must not use a string literal, assign to variable first +ℹ Suggested fix +2 2 | +3 3 | +4 4 | def f_a(): +5 |- raise RuntimeError("This is an example exception") + 5 |+ msg = "This is an example exception" + 6 |+ raise RuntimeError(msg) +6 7 | +7 8 | +8 9 | def f_a_short(): + +EM.py:9:24: EM101 [*] Exception must not use a string literal, assign to variable first | 9 | def f_a_short(): 10 | raise RuntimeError("Error") | ^^^^^^^ EM101 | + = help: Assign to variable; remove string literal + +ℹ Suggested fix +6 6 | +7 7 | +8 8 | def f_a_short(): +9 |- raise RuntimeError("Error") + 9 |+ msg = "Error" + 10 |+ raise RuntimeError(msg) +10 11 | +11 12 | +12 13 | def f_b(): -EM.py:14:24: EM102 Exception must not use an f-string literal, assign to variable first +EM.py:14:24: EM102 [*] Exception must not use an f-string literal, assign to variable first | 14 | def f_b(): 15 | example = "example" 16 | raise RuntimeError(f"This is an {example} exception") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM102 | + = help: Assign to variable; remove f-string literal -EM.py:18:24: EM103 Exception must not use a `.format()` string directly, assign to variable first +ℹ Suggested fix +11 11 | +12 12 | def f_b(): +13 13 | example = "example" +14 |- raise RuntimeError(f"This is an {example} exception") + 14 |+ msg = f"This is an {example} exception" + 15 |+ raise RuntimeError(msg) +15 16 | +16 17 | +17 18 | def f_c(): + +EM.py:18:24: EM103 [*] Exception must not use a `.format()` string directly, assign to variable first | 18 | def f_c(): 19 | raise RuntimeError("This is an {example} exception".format(example="example")) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM103 | + = help: Assign to variable; remove `.format()` string + +ℹ Suggested fix +15 15 | +16 16 | +17 17 | def f_c(): +18 |- raise RuntimeError("This is an {example} exception".format(example="example")) + 18 |+ msg = "This is an {example} exception".format(example="example") + 19 |+ raise RuntimeError(msg) +19 20 | +20 21 | +21 22 | def f_ok(): + +EM.py:28:24: EM101 Exception must not use a string literal, assign to variable first + | +28 | def f_unfixable(): +29 | msg = "hello" +30 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | + +EM.py:35:24: EM101 [*] Exception must not use a string literal, assign to variable first + | +35 | msg = "hello" +36 | +37 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | + = help: Assign to variable; remove string literal + +ℹ Suggested fix +32 32 | def nested(): +33 33 | msg = "hello" +34 34 | +35 |- raise RuntimeError("This is an example exception") + 35 |+ msg = "This is an example exception" + 36 |+ raise RuntimeError(msg) +36 37 | +37 38 | +38 39 | def f_msg_in_parent_scope(): + +EM.py:42:28: EM101 Exception must not use a string literal, assign to variable first + | +42 | def nested(): +43 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | + +EM.py:47:28: EM101 [*] Exception must not use a string literal, assign to variable first + | +47 | def f_fix_indentation_check(foo): +48 | if foo: +49 | raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 +50 | else: +51 | if foo == "foo": + | + = help: Assign to variable; remove string literal + +ℹ Suggested fix +44 44 | +45 45 | def f_fix_indentation_check(foo): +46 46 | if foo: +47 |- raise RuntimeError("This is an example exception") + 47 |+ msg = "This is an example exception" + 48 |+ raise RuntimeError(msg) +48 49 | else: +49 50 | if foo == "foo": +50 51 | raise RuntimeError(f"This is an exception: {foo}") + +EM.py:50:32: EM102 [*] Exception must not use an f-string literal, assign to variable first + | +50 | else: +51 | if foo == "foo": +52 | raise RuntimeError(f"This is an exception: {foo}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM102 +53 | raise RuntimeError("This is an exception: {}".format(foo)) + | + = help: Assign to variable; remove f-string literal + +ℹ Suggested fix +47 47 | raise RuntimeError("This is an example exception") +48 48 | else: +49 49 | if foo == "foo": +50 |- raise RuntimeError(f"This is an exception: {foo}") + 50 |+ msg = f"This is an exception: {foo}" + 51 |+ raise RuntimeError(msg) +51 52 | raise RuntimeError("This is an exception: {}".format(foo)) +52 53 | +53 54 | + +EM.py:51:24: EM103 [*] Exception must not use a `.format()` string directly, assign to variable first + | +51 | if foo == "foo": +52 | raise RuntimeError(f"This is an exception: {foo}") +53 | raise RuntimeError("This is an exception: {}".format(foo)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM103 + | + = help: Assign to variable; remove `.format()` string + +ℹ Suggested fix +48 48 | else: +49 49 | if foo == "foo": +50 50 | raise RuntimeError(f"This is an exception: {foo}") +51 |- raise RuntimeError("This is an exception: {}".format(foo)) + 51 |+ msg = "This is an exception: {}".format(foo) + 52 |+ raise RuntimeError(msg) +52 53 | +53 54 | +54 55 | # Report these, but don't fix them + +EM.py:55:28: EM101 Exception must not use a string literal, assign to variable first + | +55 | # Report these, but don't fix them +56 | if foo: raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 +57 | if foo: x = 1; raise RuntimeError("This is an example exception") + | + +EM.py:56:35: EM101 Exception must not use a string literal, assign to variable first + | +56 | # Report these, but don't fix them +57 | if foo: raise RuntimeError("This is an example exception") +58 | if foo: x = 1; raise RuntimeError("This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | From ee6d8f74678e2070bb42df31bd4fede32e6ed09c Mon Sep 17 00:00:00 2001 From: Moritz Sauter Date: Fri, 28 Apr 2023 03:23:06 +0200 Subject: [PATCH 04/32] Add bugbear immutable functions as allowed in dataclasses (#4122) --- .../resources/test/fixtures/ruff/RUF009.py | 13 +++- .../rules/function_call_argument_default.rs | 71 +++++++++++-------- .../ruff/src/rules/flake8_bugbear/settings.rs | 3 +- .../mutable_defaults_in_dataclass_fields.rs | 28 ++++++-- ..._rules__ruff__tests__RUF009_RUF009.py.snap | 48 ++++++------- .../src/analyze/typing.rs | 30 ++++++++ ruff.schema.json | 2 +- 7 files changed, 132 insertions(+), 63 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF009.py b/crates/ruff/resources/test/fixtures/ruff/RUF009.py index 9a8a9e6ee2842..53c6a0598e1bc 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF009.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF009.py @@ -1,5 +1,8 @@ +import datetime +import re import typing -from dataclasses import dataclass +from dataclasses import dataclass, field +from pathlib import Path from typing import ClassVar, NamedTuple @@ -17,6 +20,12 @@ class A: class_variable: typing.ClassVar[list[int]] = default_function() another_class_var: ClassVar[list[int]] = default_function() + fine_path: Path = Path() + fine_date: datetime.date = datetime.date(2042, 1, 1) + fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7) + fine_tuple: tuple[int] = tuple([1]) + fine_regex: re.Pattern = re.compile(r".*") + DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3]) @@ -29,3 +38,5 @@ class B: not_optimal: ImmutableType = ImmutableType(20) good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES + + fine_dataclass_function: list[int] = field(default_factory=list) diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index d255bcf9d0d26..c873bc4bd2174 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -1,3 +1,4 @@ +use ruff_python_semantic::analyze::typing::is_immutable_func; use ruff_text_size::TextRange; use rustpython_parser::ast::{Arguments, Constant, Expr, ExprKind}; @@ -13,6 +14,45 @@ use crate::checkers::ast::Checker; use super::mutable_argument_default::is_mutable_func; +/// ## What it does +/// Checks for function calls in function defaults. +/// +/// ## Why is it bad? +/// The function calls in the defaults are only performed once, at definition +/// time. The returned value is then reused by all calls to the function. +/// +/// ## Options +/// - `flake8-bugbear.extend-immutable-calls` +/// +/// ## Examples: +/// ```python +/// def create_list() -> list[int]: +/// return [1, 2, 3] +/// +/// def mutable_default(arg: list[int] = create_list()) -> list[int]: +/// arg.append(4) +/// return arg +/// ``` +/// +/// Use instead: +/// ```python +/// def better(arg: list[int] | None = None) -> list[int]: +/// if arg is None: +/// arg = create_list() +/// +/// arg.append(4) +/// return arg +/// ``` +/// +/// Alternatively, if you _want_ the shared behaviour, make it more obvious +/// by assigning it to a module-level variable: +/// ```python +/// I_KNOW_THIS_IS_SHARED_STATE = create_list() +/// +/// def mutable_default(arg: list[int] = I_KNOW_THIS_IS_SHARED_STATE) -> list[int]: +/// arg.append(4) +/// return arg +/// ``` #[violation] pub struct FunctionCallInDefaultArgument { pub name: Option, @@ -30,35 +70,6 @@ impl Violation for FunctionCallInDefaultArgument { } } -const IMMUTABLE_FUNCS: &[&[&str]] = &[ - &["", "tuple"], - &["", "frozenset"], - &["datetime", "date"], - &["datetime", "datetime"], - &["datetime", "timedelta"], - &["decimal", "Decimal"], - &["operator", "attrgetter"], - &["operator", "itemgetter"], - &["operator", "methodcaller"], - &["pathlib", "Path"], - &["types", "MappingProxyType"], - &["re", "compile"], -]; - -fn is_immutable_func(checker: &Checker, func: &Expr, extend_immutable_calls: &[CallPath]) -> bool { - checker - .ctx - .resolve_call_path(func) - .map_or(false, |call_path| { - IMMUTABLE_FUNCS - .iter() - .any(|target| call_path.as_slice() == *target) - || extend_immutable_calls - .iter() - .any(|target| call_path == *target) - }) -} - struct ArgumentDefaultVisitor<'a> { checker: &'a Checker<'a>, diagnostics: Vec<(DiagnosticKind, TextRange)>, @@ -73,7 +84,7 @@ where match &expr.node { ExprKind::Call { func, args, .. } => { if !is_mutable_func(self.checker, func) - && !is_immutable_func(self.checker, func, &self.extend_immutable_calls) + && !is_immutable_func(&self.checker.ctx, func, &self.extend_immutable_calls) && !is_nan_or_infinity(func, args) { self.diagnostics.push(( diff --git a/crates/ruff/src/rules/flake8_bugbear/settings.rs b/crates/ruff/src/rules/flake8_bugbear/settings.rs index c9a436fe02de0..566aa1519d2db 100644 --- a/crates/ruff/src/rules/flake8_bugbear/settings.rs +++ b/crates/ruff/src/rules/flake8_bugbear/settings.rs @@ -23,7 +23,8 @@ pub struct Options { "# )] /// Additional callable functions to consider "immutable" when evaluating, - /// e.g., the `no-mutable-default-argument` rule (`B006`). + /// e.g., the `no-mutable-default-argument` rule (`B006`) or + /// `no-function-call-in-dataclass-defaults` rule (`RUF009`). pub extend_immutable_calls: Option>, } diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs index 68c2c1bfecec0..3fe3e13b44a41 100644 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs +++ b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs @@ -1,10 +1,13 @@ +use ruff_python_ast::call_path::{from_qualified_name, CallPath}; use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{call_path::compose_call_path, helpers::map_callable}; -use ruff_python_semantic::analyze::typing::is_immutable_annotation; -use ruff_python_semantic::context::Context; +use ruff_python_semantic::{ + analyze::typing::{is_immutable_annotation, is_immutable_func}, + context::Context, +}; use crate::checkers::ast::Checker; @@ -64,6 +67,9 @@ impl Violation for MutableDataclassDefault { /// Function calls are only performed once, at definition time. The returned /// value is then reused by all instances of the dataclass. /// +/// ## Options +/// - `flake8-bugbear.extend-immutable-calls` +/// /// ## Examples: /// ```python /// from dataclasses import dataclass @@ -141,11 +147,11 @@ fn is_mutable_expr(expr: &Expr) -> bool { ) } -const ALLOWED_FUNCS: &[&[&str]] = &[&["dataclasses", "field"]]; +const ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS: &[&[&str]] = &[&["dataclasses", "field"]]; -fn is_allowed_func(context: &Context, func: &Expr) -> bool { +fn is_allowed_dataclass_function(context: &Context, func: &Expr) -> bool { context.resolve_call_path(func).map_or(false, |call_path| { - ALLOWED_FUNCS + ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS .iter() .any(|target| call_path.as_slice() == *target) }) @@ -161,6 +167,14 @@ fn is_class_var_annotation(context: &Context, annotation: &Expr) -> bool { /// RUF009 pub fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) { + let extend_immutable_calls: Vec = checker + .settings + .flake8_bugbear + .extend_immutable_calls + .iter() + .map(|target| from_qualified_name(target)) + .collect(); + for statement in body { if let StmtKind::AnnAssign { annotation, @@ -172,7 +186,9 @@ pub fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) continue; } if let ExprKind::Call { func, .. } = &expr.node { - if !is_allowed_func(&checker.ctx, func) { + if !is_immutable_func(&checker.ctx, func, &extend_immutable_calls) + && !is_allowed_dataclass_function(&checker.ctx, func) + { checker.diagnostics.push(Diagnostic::new( FunctionCallInDataclassDefaultArgument { name: compose_call_path(func), diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap index 4f95c1be1662d..096591e7f9be4 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap @@ -1,44 +1,44 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF009.py:16:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:19:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -16 | @dataclass() -17 | class A: -18 | hidden_mutable_default: list[int] = default_function() +19 | @dataclass() +20 | class A: +21 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 -19 | class_variable: typing.ClassVar[list[int]] = default_function() -20 | another_class_var: ClassVar[list[int]] = default_function() +22 | class_variable: typing.ClassVar[list[int]] = default_function() +23 | another_class_var: ClassVar[list[int]] = default_function() | -RUF009.py:27:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:36:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -27 | @dataclass -28 | class B: -29 | hidden_mutable_default: list[int] = default_function() +36 | @dataclass +37 | class B: +38 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 -30 | another_dataclass: A = A() -31 | not_optimal: ImmutableType = ImmutableType(20) +39 | another_dataclass: A = A() +40 | not_optimal: ImmutableType = ImmutableType(20) | -RUF009.py:28:28: RUF009 Do not perform function call `A` in dataclass defaults +RUF009.py:37:28: RUF009 Do not perform function call `A` in dataclass defaults | -28 | class B: -29 | hidden_mutable_default: list[int] = default_function() -30 | another_dataclass: A = A() +37 | class B: +38 | hidden_mutable_default: list[int] = default_function() +39 | another_dataclass: A = A() | ^^^ RUF009 -31 | not_optimal: ImmutableType = ImmutableType(20) -32 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +40 | not_optimal: ImmutableType = ImmutableType(20) +41 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES | -RUF009.py:29:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults +RUF009.py:38:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults | -29 | hidden_mutable_default: list[int] = default_function() -30 | another_dataclass: A = A() -31 | not_optimal: ImmutableType = ImmutableType(20) +38 | hidden_mutable_default: list[int] = default_function() +39 | another_dataclass: A = A() +40 | not_optimal: ImmutableType = ImmutableType(20) | ^^^^^^^^^^^^^^^^^ RUF009 -32 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -33 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES +41 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +42 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES | diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 3c45adb9015bc..850e1c03bc6eb 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -122,3 +122,33 @@ pub fn is_immutable_annotation(context: &Context, expr: &Expr) -> bool { _ => false, } } + +const IMMUTABLE_FUNCS: &[&[&str]] = &[ + &["", "tuple"], + &["", "frozenset"], + &["datetime", "date"], + &["datetime", "datetime"], + &["datetime", "timedelta"], + &["decimal", "Decimal"], + &["operator", "attrgetter"], + &["operator", "itemgetter"], + &["operator", "methodcaller"], + &["pathlib", "Path"], + &["types", "MappingProxyType"], + &["re", "compile"], +]; + +pub fn is_immutable_func( + context: &Context, + func: &Expr, + extend_immutable_calls: &[CallPath], +) -> bool { + context.resolve_call_path(func).map_or(false, |call_path| { + IMMUTABLE_FUNCS + .iter() + .any(|target| call_path.as_slice() == *target) + || extend_immutable_calls + .iter() + .any(|target| call_path == *target) + }) +} diff --git a/ruff.schema.json b/ruff.schema.json index 2d126f00f9dd8..6e9f2d2ef9840 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -669,7 +669,7 @@ "type": "object", "properties": { "extend-immutable-calls": { - "description": "Additional callable functions to consider \"immutable\" when evaluating, e.g., the `no-mutable-default-argument` rule (`B006`).", + "description": "Additional callable functions to consider \"immutable\" when evaluating, e.g., the `no-mutable-default-argument` rule (`B006`) or `no-function-call-in-dataclass-defaults` rule (`RUF009`).", "type": [ "array", "null" From b34804ceb5986e7809e4fb6771170001631899bb Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Thu, 27 Apr 2023 20:24:35 -0500 Subject: [PATCH 05/32] Make D410/D411 autofixes mutually exclusive (#4110) --- .../src/rules/pydocstyle/rules/sections.rs | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index abfd5b827aa8e..5a5b55b8b7716 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -580,7 +580,12 @@ fn blanks_and_section_underline( } } -fn common_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { +fn common_section( + checker: &mut Checker, + docstring: &Docstring, + context: &SectionContext, + next: Option<&SectionContext>, +) { if checker.settings.rules.enabled(Rule::CapitalizeSectionName) { let capitalized_section_name = context.kind().as_str(); if context.section_name() != capitalized_section_name { @@ -626,24 +631,21 @@ fn common_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio let line_end = checker.stylist.line_ending().as_str(); let last_line = context.following_lines().last(); if last_line.map_or(true, |line| !line.trim().is_empty()) { - if context.is_last() { + if let Some(next) = next { if checker .settings .rules - .enabled(Rule::BlankLineAfterLastSection) + .enabled(Rule::NoBlankLineAfterSection) { let mut diagnostic = Diagnostic::new( - BlankLineAfterLastSection { + NoBlankLineAfterSection { name: context.section_name().to_string(), }, docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { - // Add a newline after the section. - diagnostic.set_fix(Edit::insertion( - format!("{}{}", line_end, docstring.indentation), - context.range().end(), - )); + // Add a newline at the beginning of the next section. + diagnostic.set_fix(Edit::insertion(line_end.to_string(), next.range().start())); } checker.diagnostics.push(diagnostic); } @@ -651,18 +653,20 @@ fn common_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio if checker .settings .rules - .enabled(Rule::NoBlankLineAfterSection) + .enabled(Rule::BlankLineAfterLastSection) { let mut diagnostic = Diagnostic::new( - NoBlankLineAfterSection { + BlankLineAfterLastSection { name: context.section_name().to_string(), }, docstring.range(), ); if checker.patch(diagnostic.kind.rule()) { // Add a newline after the section. - diagnostic - .set_fix(Edit::insertion(line_end.to_string(), context.range().end())); + diagnostic.set_fix(Edit::insertion( + format!("{}{}", line_end, docstring.indentation), + context.range().end(), + )); } checker.diagnostics.push(diagnostic); } @@ -861,8 +865,13 @@ fn parameters_section(checker: &mut Checker, docstring: &Docstring, context: &Se missing_args(checker, docstring, &docstring_args); } -fn numpy_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { - common_section(checker, docstring, context); +fn numpy_section( + checker: &mut Checker, + docstring: &Docstring, + context: &SectionContext, + next: Option<&SectionContext>, +) { + common_section(checker, docstring, context, next); if checker .settings @@ -897,8 +906,13 @@ fn numpy_section(checker: &mut Checker, docstring: &Docstring, context: &Section } } -fn google_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { - common_section(checker, docstring, context); +fn google_section( + checker: &mut Checker, + docstring: &Docstring, + context: &SectionContext, + next: Option<&SectionContext>, +) { + common_section(checker, docstring, context, next); if checker.settings.rules.enabled(Rule::SectionNameEndsInColon) { let suffix = context.summary_after_section_name(); @@ -927,8 +941,9 @@ fn parse_numpy_sections( docstring: &Docstring, section_contexts: &SectionContexts, ) { - for section_context in section_contexts { - numpy_section(checker, docstring, §ion_context); + let mut iterator = section_contexts.iter().peekable(); + while let Some(context) = iterator.next() { + numpy_section(checker, docstring, &context, iterator.peek()); } } @@ -937,8 +952,9 @@ fn parse_google_sections( docstring: &Docstring, section_contexts: &SectionContexts, ) { - for section_context in section_contexts { - google_section(checker, docstring, §ion_context); + let mut iterator = section_contexts.iter().peekable(); + while let Some(context) = iterator.next() { + google_section(checker, docstring, &context, iterator.peek()); } if checker.settings.rules.enabled(Rule::UndocumentedParam) { From 432ea6f2e28d2a54bc2071ccb995fefeb6d85d92 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 27 Apr 2023 21:29:03 -0400 Subject: [PATCH 06/32] Tweak rule documentation for `B008` (#4137) --- .../rules/function_call_argument_default.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index c873bc4bd2174..4d775e60f247e 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -1,4 +1,3 @@ -use ruff_python_semantic::analyze::typing::is_immutable_func; use ruff_text_size::TextRange; use rustpython_parser::ast::{Arguments, Constant, Expr, ExprKind}; @@ -9,22 +8,23 @@ use ruff_python_ast::call_path::from_qualified_name; use ruff_python_ast::call_path::{compose_call_path, CallPath}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; +use ruff_python_semantic::analyze::typing::is_immutable_func; use crate::checkers::ast::Checker; - -use super::mutable_argument_default::is_mutable_func; +use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_func; /// ## What it does -/// Checks for function calls in function defaults. +/// Checks for function calls in default function arguments. /// /// ## Why is it bad? -/// The function calls in the defaults are only performed once, at definition -/// time. The returned value is then reused by all calls to the function. +/// Any function call that's used in a default argument will only be performed +/// once, at definition time. The returned value will then be reused by all +/// calls to the function, which can lead to unexpected behaviour. /// /// ## Options /// - `flake8-bugbear.extend-immutable-calls` /// -/// ## Examples: +/// ## Example /// ```python /// def create_list() -> list[int]: /// return [1, 2, 3] @@ -44,8 +44,8 @@ use super::mutable_argument_default::is_mutable_func; /// return arg /// ``` /// -/// Alternatively, if you _want_ the shared behaviour, make it more obvious -/// by assigning it to a module-level variable: +/// Alternatively, if shared behavior is desirable, clarify the intent by +/// assigning to a module-level variable: /// ```python /// I_KNOW_THIS_IS_SHARED_STATE = create_list() /// From 12d64a223bd1075f877e73b387921113444a2ec5 Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Fri, 28 Apr 2023 23:14:15 +0100 Subject: [PATCH 07/32] Document RUF100 (#4141) --- .../ruff/src/rules/ruff/rules/unused_noqa.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/ruff/src/rules/ruff/rules/unused_noqa.rs b/crates/ruff/src/rules/ruff/rules/unused_noqa.rs index d79b5aa0c1039..6bce540d23691 100644 --- a/crates/ruff/src/rules/ruff/rules/unused_noqa.rs +++ b/crates/ruff/src/rules/ruff/rules/unused_noqa.rs @@ -10,6 +10,31 @@ pub struct UnusedCodes { pub unmatched: Vec, } +/// ## What it does +/// Checks for `noqa` directives that are no longer applicable. +/// +/// ## Why is this bad? +/// A `noqa` directive that no longer matches any diagnostic violations is +/// likely included by mistake, and should be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// import foo # noqa: F401 +/// +/// def bar(): +/// foo.bar() +/// ``` +/// +/// Use instead: +/// ```python +/// import foo +/// +/// def bar(): +/// foo.bar() +/// ``` +/// +/// ## References +/// - [Automatic `noqa` management](https://beta.ruff.rs/docs/configuration/#automatic-noqa-management) #[violation] pub struct UnusedNOQA { pub codes: Option, From 0172cc51a7057c1ca8e4c2a2247f0d2dd621c78f Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Sat, 29 Apr 2023 04:19:00 +0100 Subject: [PATCH 08/32] Document `flake8-print` (#4144) --- .../rules/flake8_print/rules/print_call.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/ruff/src/rules/flake8_print/rules/print_call.rs b/crates/ruff/src/rules/flake8_print/rules/print_call.rs index 150609beb6f50..80d5046318e2c 100644 --- a/crates/ruff/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff/src/rules/flake8_print/rules/print_call.rs @@ -7,6 +7,27 @@ use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for `print` statements. +/// +/// ## Why is this bad? +/// `print` statements are useful in some situations (e.g., debugging), but +/// should typically be omitted from production code. `print` statements can +/// lead to the accidental inclusion of sensitive information in logs, and are +/// not configurable by clients, unlike `logging` statements. +/// +/// ## Example +/// ```python +/// def add_numbers(a, b): +/// print(f"The sum of {a} and {b} is {a + b}") +/// return a + b +/// ``` +/// +/// Use instead: +/// ```python +/// def add_numbers(a, b): +/// return a + b +/// ``` #[violation] pub struct Print; @@ -17,6 +38,33 @@ impl Violation for Print { } } +/// ## What it does +/// Checks for `pprint` statements. +/// +/// ## Why is this bad? +/// Like `print` statements, `pprint` statements are useful in some situations +/// (e.g., debugging), but should typically be omitted from production code. +/// `pprint` statements can lead to the accidental inclusion of sensitive +/// information in logs, and are not configurable by clients, unlike `logging` +/// statements. +/// +/// ## Example +/// ```python +/// import pprint +/// +/// +/// def merge_dicts(dict_a, dict_b): +/// dict_c = {**dict_a, **dict_b} +/// pprint.pprint(dict_c) +/// return dict_c +/// ``` +/// +/// Use instead: +/// ```python +/// def merge_dicts(dict_a, dict_b): +/// dict_c = {**dict_a, **dict_b} +/// return dict_c +/// ``` #[violation] pub struct PPrint; From 03144b2fad6cf00f6a15aac62f45dc775b13ceb2 Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Sat, 29 Apr 2023 04:24:15 +0100 Subject: [PATCH 09/32] Document `flake8-commas` (#4142) --- crates/ruff/src/rules/flake8_commas/rules.rs | 78 ++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/ruff/src/rules/flake8_commas/rules.rs b/crates/ruff/src/rules/flake8_commas/rules.rs index 3f0ea6246d264..64228455af7fc 100644 --- a/crates/ruff/src/rules/flake8_commas/rules.rs +++ b/crates/ruff/src/rules/flake8_commas/rules.rs @@ -110,6 +110,29 @@ impl Context { } } +/// ## What it does +/// Checks for the absence of trailing commas. +/// +/// ## Why is this bad? +/// The presence of a trailing comma can reduce diff size when parameters or +/// elements are added or removed from function calls, function definitions, +/// literals, etc. +/// +/// ## Example +/// ```python +/// foo = { +/// "bar": 1, +/// "baz": 2 +/// } +/// ``` +/// +/// Use instead: +/// ```python +/// foo = { +/// "bar": 1, +/// "baz": 2, +/// } +/// ``` #[violation] pub struct MissingTrailingComma; @@ -124,6 +147,45 @@ impl AlwaysAutofixableViolation for MissingTrailingComma { } } +/// ## What it does +/// Checks for the presence of trailing commas on bare (i.e., unparenthesized) +/// tuples. +/// +/// ## Why is this bad? +/// The presence of a misplaced comma will cause Python to interpret the value +/// as a tuple, which can lead to unexpected behaviour. +/// +/// ## Example +/// ```python +/// import json +/// +/// +/// foo = json.dumps({ +/// "bar": 1, +/// }), +/// ``` +/// +/// Use instead: +/// ```python +/// import json +/// +/// +/// foo = json.dumps({ +/// "bar": 1, +/// }) +/// ``` +/// +/// In the event that a tuple is intended, then use instead: +/// ```python +/// import json +/// +/// +/// foo = ( +/// json.dumps({ +/// "bar": 1, +/// }), +/// ) +/// ``` #[violation] pub struct TrailingCommaOnBareTuple; @@ -134,6 +196,22 @@ impl Violation for TrailingCommaOnBareTuple { } } +/// ## What it does +/// Checks for the presence of prohibited trailing commas. +/// +/// ## Why is this bad? +/// Trailing commas are not essential in some cases and can therefore be viewed +/// as unnecessary. +/// +/// ## Example +/// ```python +/// foo = (1, 2, 3,) +/// ``` +/// +/// Use instead: +/// ```python +/// foo = (1, 2, 3) +/// ``` #[violation] pub struct ProhibitedTrailingComma; From f0f4bf2929592433c634cfcc73ac04510ec9f40e Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Sat, 29 Apr 2023 17:13:35 +0100 Subject: [PATCH 10/32] Move typos to pre-commit config (#4148) --- .github/workflows/ci.yaml | 9 --------- .pre-commit-config.yaml | 5 +++++ _typos.toml | 1 + .../test/fixtures/black/simple_cases/comments4.py | 2 +- .../fixtures/black/simple_cases/comments4.py.expect | 2 +- ...hon_formatter__tests__black_test__comments4_py.snap | 10 +++++----- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 514e46136227e..2a75bd59ada7b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -121,15 +121,6 @@ jobs: - run: cargo check - run: cargo fmt --all --check - typos: - name: "spell check" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: crate-ci/typos@master - with: - files: . - ecosystem: name: "ecosystem" runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da2807fd09346..bb10740493b17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,11 @@ repos: - MD033 # no-inline-html - -- + - repo: https://github.com/crate-ci/typos + rev: v1.14.8 + hooks: + - id: typos + - repo: local hooks: - id: cargo-fmt diff --git a/_typos.toml b/_typos.toml index c0c02513b57db..8f5f834f9b409 100644 --- a/_typos.toml +++ b/_typos.toml @@ -7,3 +7,4 @@ hel = "hel" whos = "whos" spawnve = "spawnve" ned = "ned" +poit = "poit" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py index 2147d41c9da74..9f4f39d83599d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py @@ -85,7 +85,7 @@ def foo2(list_a, list_b): def foo3(list_a, list_b): return ( - # Standlone comment but weirdly placed. + # Standalone comment but weirdly placed. User.query.filter(User.foo == "bar") .filter( db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py.expect index 2147d41c9da74..9f4f39d83599d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py.expect @@ -85,7 +85,7 @@ def foo2(list_a, list_b): def foo3(list_a, list_b): return ( - # Standlone comment but weirdly placed. + # Standalone comment but weirdly placed. User.query.filter(User.foo == "bar") .filter( db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap index 662947d62fed4..d246955bc6bef 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap @@ -93,7 +93,7 @@ def foo2(list_a, list_b): def foo3(list_a, list_b): return ( - # Standlone comment but weirdly placed. + # Standalone comment but weirdly placed. User.query.filter(User.foo == "bar") .filter( db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) @@ -221,14 +221,14 @@ def foo3(list_a, list_b): def foo3(list_a, list_b): - return ( -- # Standlone comment but weirdly placed. +- # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -+ return # Standlone comment but weirdly placed. ++ return # Standalone comment but weirdly placed. + User.query.filter(User.foo == "bar").filter( + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) + ).filter(User.xyz.is_(None)) @@ -327,7 +327,7 @@ def foo2(list_a, list_b): def foo3(list_a, list_b): - return # Standlone comment but weirdly placed. + return # Standalone comment but weirdly placed. User.query.filter(User.foo == "bar").filter( db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ).filter(User.xyz.is_(None)) @@ -423,7 +423,7 @@ def foo2(list_a, list_b): def foo3(list_a, list_b): return ( - # Standlone comment but weirdly placed. + # Standalone comment but weirdly placed. User.query.filter(User.foo == "bar") .filter( db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) From 8f61eae1e7ce732f78c9d4574ee684ae17f4aa77 Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Sat, 29 Apr 2023 20:13:10 +0100 Subject: [PATCH 11/32] Add remaining `pep8-naming` docs (#4149) --- .../rules/invalid_argument_name.rs | 28 +++++++++++++ .../mixed_case_variable_in_class_scope.rs | 30 ++++++++++++++ .../mixed_case_variable_in_global_scope.rs | 40 +++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs index 94d04aefe2be9..58b9ebf971d1a 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -3,6 +3,34 @@ use rustpython_parser::ast::Arg; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for argument names that do not follow the `snake_case` convention. +/// +/// ## Why is this bad? +/// [PEP 8] recommends that function names should be lower case and separated +/// by underscores (also known as `snake_case`). +/// +/// > Function names should be lowercase, with words separated by underscores +/// as necessary to improve readability. +/// > +/// > Variable names follow the same convention as function names. +/// > +/// > mixedCase is allowed only in contexts where that’s already the +/// prevailing style (e.g. threading.py), to retain backwards compatibility. +/// +/// ## Example +/// ```python +/// def MY_FUNCTION(): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// def my_function(): +/// pass +/// ``` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[violation] pub struct InvalidArgumentName { pub name: String, diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs index 1bd1c24cf9d1c..52cc00ce950e8 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs @@ -6,6 +6,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; +/// ## What it does +/// Checks for class variable names that follow the `mixedCase` convention. +/// +/// ## Why is this bad? +/// [PEP 8] recommends that variable names should be lower case and separated +/// by underscores (also known as `snake_case`). +/// +/// > Function names should be lowercase, with words separated by underscores +/// as necessary to improve readability. +/// > +/// > Variable names follow the same convention as function names. +/// > +/// > mixedCase is allowed only in contexts where that’s already the +/// prevailing style (e.g. threading.py), to retain backwards compatibility. +/// +/// ## Example +/// ```python +/// class MyClass: +/// myVariable = "hello" +/// another_variable = "world" +/// ``` +/// +/// Use instead: +/// ```python +/// class MyClass: +/// my_variable = "hello" +/// another_variable = "world" +/// ``` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[violation] pub struct MixedCaseVariableInClassScope { pub name: String, diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs index 470f809f97064..f5b5f7d54eabd 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs @@ -6,6 +6,46 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; +/// ## What it does +/// Checks for global variable names that follow the `mixedCase` convention. +/// +/// ## Why is this bad? +/// [PEP 8] recommends that global variable names should be lower case and +/// separated by underscores (also known as `snake_case`). +/// +/// > ### Global Variable Names +/// > (Let’s hope that these variables are meant for use inside one module +/// only.) The conventions are about the same as those for functions. +/// > +/// > Modules that are designed for use via from M import * should use the +/// __all__ mechanism to prevent exporting globals, or use the older +/// convention of prefixing such globals with an underscore (which you might +///want to do to indicate these globals are “module non-public”). +/// > +/// > ### Function and Variable Names +/// > Function names should be lowercase, with words separated by underscores +/// as necessary to improve readability. +/// > +/// > Variable names follow the same convention as function names. +/// > +/// > mixedCase is allowed only in contexts where that’s already the prevailing +/// style (e.g. threading.py), to retain backwards compatibility. +/// +/// ## Example +/// ```python +/// myVariable = "hello" +/// another_variable = "world" +/// yet_anotherVariable = "foo" +/// ``` +/// +/// Use instead: +/// ```python +/// my_variable = "hello" +/// another_variable = "world" +/// yet_another_variable = "foo" +/// ``` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#global-variable-names #[violation] pub struct MixedCaseVariableInGlobalScope { pub name: String, From 39ed75f6434e16ba5cda0d8a05b97c120f6b1f0e Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Sat, 29 Apr 2023 20:17:50 +0100 Subject: [PATCH 12/32] Document `flake8-unused-arguments` (#4147) --- .../rules/flake8_unused_arguments/rules.rs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules.rs index f67b362775204..ae2f505a2d86e 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules.rs @@ -16,6 +16,24 @@ use crate::checkers::ast::Checker; use super::helpers; use super::types::Argumentable; +/// ## What it does +/// Checks for the presence of unused arguments in function definitions. +/// +/// ## Why is this bad? +/// An argument that is defined but not used is likely a mistake, and should +/// be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// def foo(bar, baz): +/// return bar * 2 +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar): +/// return bar * 2 +/// ``` #[violation] pub struct UnusedFunctionArgument { pub name: String, @@ -29,6 +47,25 @@ impl Violation for UnusedFunctionArgument { } } +/// ## What it does +/// Checks for the presence of unused arguments in instance method definitions. +/// +/// ## Why is this bad? +/// An argument that is defined but not used is likely a mistake, and should +/// be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// class MyClass: +/// def my_method(self, arg1, arg2): +/// print(arg1) +/// ``` +/// +/// Use instead: +/// ```python +/// class MyClass: +/// def my_method(self, arg1): +/// ``` #[violation] pub struct UnusedMethodArgument { pub name: String, @@ -42,6 +79,34 @@ impl Violation for UnusedMethodArgument { } } +/// ## What it does +/// Checks for the presence of unused arguments in class method definitions. +/// +/// ## Why is this bad? +/// An argument that is defined but not used is likely a mistake, and should +/// be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// class MyClass: +/// @classmethod +/// def my_method(self, arg1, arg2): +/// print(arg1) +/// +/// def other_method(self): +/// self.my_method("foo", "bar") +/// ``` +/// +/// Use instead: +/// ```python +/// class MyClass: +/// @classmethod +/// def my_method(self, arg1): +/// print(arg1) +/// +/// def other_method(self): +/// self.my_method("foo", "bar") +/// ``` #[violation] pub struct UnusedClassMethodArgument { pub name: String, @@ -55,6 +120,34 @@ impl Violation for UnusedClassMethodArgument { } } +/// ## What it does +/// Checks for the presence of unused arguments in static method definitions. +/// +/// ## Why is this bad? +/// An argument that is defined but not used is likely a mistake, and should +/// be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// class MyClass: +/// @staticmethod +/// def my_static_method(self, arg1, arg2): +/// print(arg1) +/// +/// def other_method(self): +/// self.my_static_method("foo", "bar") +/// ``` +/// +/// Use instead: +/// ```python +/// class MyClass: +/// @static +/// def my_static_method(self, arg1): +/// print(arg1) +/// +/// def other_method(self): +/// self.my_static_method("foo", "bar") +/// ``` #[violation] pub struct UnusedStaticMethodArgument { pub name: String, @@ -68,6 +161,25 @@ impl Violation for UnusedStaticMethodArgument { } } +/// ## What it does +/// Checks for the presence of unused arguments in lambda expression +/// definitions. +/// +/// ## Why is this bad? +/// An argument that is defined but not used is likely a mistake, and should +/// be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// my_list = [1, 2, 3, 4, 5] +/// squares = map(lambda x, y: x**2, my_list) +/// ``` +/// +/// Use instead: +/// ```python +/// my_list = [1, 2, 3, 4, 5] +/// squares = map(lambda x: x**2, my_list) +/// ``` #[violation] pub struct UnusedLambdaArgument { pub name: String, From 2115d99c43455edc0e95c6a493486e69930f8378 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 29 Apr 2023 18:23:51 -0400 Subject: [PATCH 13/32] Remove `ScopeStack` in favor of child-parent `ScopeId` pointers (#4138) --- crates/ruff/src/checkers/ast/deferred.rs | 4 +- crates/ruff/src/checkers/ast/mod.rs | 200 +++++++----------- .../pyflakes/rules/return_outside_function.rs | 16 +- .../rules/pyflakes/rules/undefined_local.rs | 53 +++-- crates/ruff/src/rules/pylint/helpers.rs | 2 +- crates/ruff_python_semantic/src/context.rs | 58 ++--- crates/ruff_python_semantic/src/scope.rs | 74 +++---- 7 files changed, 173 insertions(+), 234 deletions(-) diff --git a/crates/ruff/src/checkers/ast/deferred.rs b/crates/ruff/src/checkers/ast/deferred.rs index 44d607fbc3970..61f2135f9fcbd 100644 --- a/crates/ruff/src/checkers/ast/deferred.rs +++ b/crates/ruff/src/checkers/ast/deferred.rs @@ -1,14 +1,14 @@ -use ruff_python_semantic::scope::ScopeStack; use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, Stmt}; use ruff_python_ast::types::RefEquality; use ruff_python_semantic::analyze::visibility::{Visibility, VisibleScope}; +use ruff_python_semantic::scope::ScopeId; use crate::checkers::ast::AnnotationContext; use crate::docstrings::definition::Definition; -type Context<'a> = (ScopeStack, Vec>); +type Context<'a> = (ScopeId, Vec>); /// A collection of AST nodes that are deferred for later analysis. /// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 0a20f2a5669b5..8594d58dd8908 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1,4 +1,3 @@ -use std::iter; use std::path::Path; use itertools::Itertools; @@ -26,9 +25,7 @@ use ruff_python_semantic::binding::{ Importation, StarImportation, SubmoduleImportation, }; use ruff_python_semantic::context::Context; -use ruff_python_semantic::scope::{ - ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind, ScopeStack, -}; +use ruff_python_semantic::scope::{ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::path::is_python_stub_file; @@ -211,8 +208,7 @@ where &stmt.node, StmtKind::Import { .. } | StmtKind::ImportFrom { .. } ) { - let scope_index = self.ctx.scope_id(); - if scope_index.is_global() && self.ctx.current_stmt_parent().is_none() { + if self.ctx.scope_id.is_global() && self.ctx.current_stmt_parent().is_none() { self.importer.visit_import(stmt); } } @@ -220,14 +216,13 @@ where // Pre-visit. match &stmt.node { StmtKind::Global { names } => { - let scope_index = self.ctx.scope_id(); let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !scope_index.is_global() { + if !self.ctx.scope_id.is_global() { // Add the binding to the current scope. let context = self.ctx.execution_context(); let exceptions = self.ctx.exceptions(); - let scope = &mut self.ctx.scopes[scope_index]; - let usage = Some((scope.id, stmt.range())); + let scope = &mut self.ctx.scopes[self.ctx.scope_id]; + let usage = Some((self.ctx.scope_id, stmt.range())); for (name, range) in names.iter().zip(ranges.iter()) { let id = self.ctx.bindings.push(Binding { kind: BindingKind::Global, @@ -251,13 +246,12 @@ where } } StmtKind::Nonlocal { names } => { - let scope_index = self.ctx.scope_id(); let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !scope_index.is_global() { + if !self.ctx.scope_id.is_global() { let context = self.ctx.execution_context(); let exceptions = self.ctx.exceptions(); - let scope = &mut self.ctx.scopes[scope_index]; - let usage = Some((scope.id, stmt.range())); + let scope = &mut self.ctx.scopes[self.ctx.scope_id]; + let usage = Some((self.ctx.scope_id, stmt.range())); for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. let id = self.ctx.bindings.push(Binding { @@ -276,20 +270,18 @@ where // Mark the binding in the defining scopes as used too. (Skip the global scope // and the current scope.) for (name, range) in names.iter().zip(ranges.iter()) { - let mut exists = false; - let mut scopes_iter = self.ctx.scope_stack.iter(); - // Skip the global scope - scopes_iter.next_back(); - - for index in scopes_iter.skip(1) { - if let Some(index) = self.ctx.scopes[*index].get(name.as_str()) { - exists = true; - self.ctx.bindings[*index].runtime_usage = usage; - } - } - - // Ensure that every nonlocal has an existing binding from a parent scope. - if !exists { + let binding_id = self + .ctx + .scopes + .ancestors(self.ctx.scope_id) + .skip(1) + .take_while(|scope| !scope.kind.is_module()) + .find_map(|scope| scope.get(name.as_str())); + + if let Some(binding_id) = binding_id { + self.ctx.bindings[*binding_id].runtime_usage = usage; + } else { + // Ensure that every nonlocal has an existing binding from a parent scope. if self.settings.rules.enabled(Rule::NonlocalWithoutBinding) { self.diagnostics.push(Diagnostic::new( pylint::rules::NonlocalWithoutBinding { @@ -909,7 +901,7 @@ where kind: BindingKind::FutureImportation, runtime_usage: None, // Always mark `__future__` imports as used. - synthetic_usage: Some((self.ctx.scope_id(), alias.range())), + synthetic_usage: Some((self.ctx.scope_id, alias.range())), typing_usage: None, range: alias.range(), source: Some(*self.ctx.current_stmt()), @@ -964,7 +956,7 @@ where kind: BindingKind::Importation(Importation { name, full_name }), runtime_usage: None, synthetic_usage: if is_explicit_reexport { - Some((self.ctx.scope_id(), alias.range())) + Some((self.ctx.scope_id, alias.range())) } else { None }, @@ -1220,7 +1212,7 @@ where kind: BindingKind::FutureImportation, runtime_usage: None, // Always mark `__future__` imports as used. - synthetic_usage: Some((self.ctx.scope_id(), alias.range())), + synthetic_usage: Some((self.ctx.scope_id, alias.range())), typing_usage: None, range: alias.range(), source: Some(*self.ctx.current_stmt()), @@ -1313,7 +1305,7 @@ where }), runtime_usage: None, synthetic_usage: if is_explicit_reexport { - Some((self.ctx.scope_id(), alias.range())) + Some((self.ctx.scope_id, alias.range())) } else { None }, @@ -1702,10 +1694,9 @@ where .. } => { if self.settings.rules.enabled(Rule::UnusedLoopControlVariable) { - self.deferred.for_loops.push(( - stmt, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), - )); + self.deferred + .for_loops + .push((stmt, (self.ctx.scope_id, self.ctx.parents.clone()))); } if self .settings @@ -1986,7 +1977,7 @@ where self.deferred.definitions.push(( definition, scope.visibility, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); self.ctx.visible_scope = scope; @@ -2024,7 +2015,7 @@ where self.deferred.functions.push(( stmt, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), self.ctx.visible_scope, )); } @@ -2049,7 +2040,7 @@ where self.deferred.definitions.push(( definition, scope.visibility, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); self.ctx.visible_scope = scope; @@ -2264,13 +2255,13 @@ where expr.range(), value, (self.ctx.in_annotation, self.ctx.in_type_checking_block), - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); } else { self.deferred.type_definitions.push(( expr, (self.ctx.in_annotation, self.ctx.in_type_checking_block), - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); } return; @@ -3498,7 +3489,7 @@ where expr.range(), value, (self.ctx.in_annotation, self.ctx.in_type_checking_block), - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); } if self @@ -3619,10 +3610,9 @@ where // Recurse. match &expr.node { ExprKind::Lambda { .. } => { - self.deferred.lambdas.push(( - expr, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), - )); + self.deferred + .lambdas + .push((expr, (self.ctx.scope_id, self.ctx.parents.clone()))); } ExprKind::IfExp { test, body, orelse } => { visit_boolean_test!(self, test); @@ -4183,18 +4173,16 @@ where impl<'a> Checker<'a> { fn add_binding(&mut self, name: &'a str, binding: Binding<'a>) { let binding_id = self.ctx.bindings.next_id(); - if let Some((stack_index, existing_binding_index)) = self + if let Some((stack_index, existing_binding_id)) = self .ctx - .scope_stack - .iter() + .scopes + .ancestors(self.ctx.scope_id) .enumerate() - .find_map(|(stack_index, scope_index)| { - self.ctx.scopes[*scope_index] - .get(name) - .map(|binding_id| (stack_index, *binding_id)) + .find_map(|(stack_index, scope)| { + scope.get(name).map(|binding_id| (stack_index, *binding_id)) }) { - let existing = &self.ctx.bindings[existing_binding_index]; + let existing = &self.ctx.bindings[existing_binding_id]; let in_current_scope = stack_index == 0; if !existing.kind.is_builtin() && existing.source.map_or(true, |left| { @@ -4271,7 +4259,7 @@ impl<'a> Checker<'a> { } else if existing_is_import && binding.redefines(existing) { self.ctx .shadowed_bindings - .entry(existing_binding_index) + .entry(existing_binding_id) .or_insert_with(Vec::new) .push(binding_id); } @@ -4319,8 +4307,7 @@ impl<'a> Checker<'a> { } fn bind_builtins(&mut self) { - let scope = - &mut self.ctx.scopes[self.ctx.scope_stack.top().expect("No current scope found")]; + let scope = &mut self.ctx.scopes[self.ctx.scope_id]; for builtin in BUILTINS .iter() @@ -4346,16 +4333,13 @@ impl<'a> Checker<'a> { let ExprKind::Name { id, .. } = &expr.node else { return; }; - let scope_id = self.ctx.scope_id(); let mut first_iter = true; let mut in_generator = false; let mut import_starred = false; - for scope_index in self.ctx.scope_stack.iter() { - let scope = &self.ctx.scopes[*scope_index]; - - if matches!(scope.kind, ScopeKind::Class(_)) { + for scope in self.ctx.scopes.ancestors(self.ctx.scope_id) { + if scope.kind.is_class() { if id == "__class__" { return; } else if !first_iter && !in_generator { @@ -4366,7 +4350,7 @@ impl<'a> Checker<'a> { if let Some(index) = scope.get(id.as_str()) { // Mark the binding as used. let context = self.ctx.execution_context(); - self.ctx.bindings[*index].mark_used(scope_id, expr.range(), context); + self.ctx.bindings[*index].mark_used(self.ctx.scope_id, expr.range(), context); if self.ctx.bindings[*index].kind.is_annotation() && self.ctx.in_deferred_string_type_definition.is_none() @@ -4396,7 +4380,7 @@ impl<'a> Checker<'a> { // Mark the sub-importation as used. if let Some(index) = scope.get(full_name) { self.ctx.bindings[*index].mark_used( - scope_id, + self.ctx.scope_id, expr.range(), context, ); @@ -4413,7 +4397,7 @@ impl<'a> Checker<'a> { // Mark the sub-importation as used. if let Some(index) = scope.get(full_name.as_str()) { self.ctx.bindings[*index].mark_used( - scope_id, + self.ctx.scope_id, expr.range(), context, ); @@ -4494,18 +4478,7 @@ impl<'a> Checker<'a> { let parent = self.ctx.current_stmt().0; if self.settings.rules.enabled(Rule::UndefinedLocal) { - let scopes: Vec<&Scope> = self - .ctx - .scope_stack - .iter() - .rev() - .map(|index| &self.ctx.scopes[*index]) - .collect(); - if let Some(diagnostic) = - pyflakes::rules::undefined_local(id, &scopes, &self.ctx.bindings) - { - self.diagnostics.push(diagnostic); - } + pyflakes::rules::undefined_local(self, id); } if self @@ -4743,7 +4716,7 @@ impl<'a> Checker<'a> { docstring, }, self.ctx.visible_scope.visibility, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); docstring.is_some() } @@ -4751,10 +4724,10 @@ impl<'a> Checker<'a> { fn check_deferred_type_definitions(&mut self) { while !self.deferred.type_definitions.is_empty() { let type_definitions = std::mem::take(&mut self.deferred.type_definitions); - for (expr, (in_annotation, in_type_checking_block), (scopes, parents)) in + for (expr, (in_annotation, in_type_checking_block), (scope_id, parents)) in type_definitions { - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; self.ctx.in_annotation = in_annotation; self.ctx.in_type_checking_block = in_type_checking_block; @@ -4770,7 +4743,7 @@ impl<'a> Checker<'a> { fn check_deferred_string_type_definitions(&mut self, allocator: &'a typed_arena::Arena) { while !self.deferred.string_type_definitions.is_empty() { let type_definitions = std::mem::take(&mut self.deferred.string_type_definitions); - for (range, value, (in_annotation, in_type_checking_block), (scopes, parents)) in + for (range, value, (in_annotation, in_type_checking_block), (scope_id, parents)) in type_definitions { if let Ok((expr, kind)) = parse_type_annotation(value, range, self.locator) { @@ -4782,7 +4755,7 @@ impl<'a> Checker<'a> { let expr = allocator.alloc(expr); - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; self.ctx.in_annotation = in_annotation; self.ctx.in_type_checking_block = in_type_checking_block; @@ -4812,10 +4785,9 @@ impl<'a> Checker<'a> { fn check_deferred_functions(&mut self) { while !self.deferred.functions.is_empty() { let deferred_functions = std::mem::take(&mut self.deferred.functions); - for (stmt, (scopes, parents), visibility) in deferred_functions { - let scope_snapshot = scopes.snapshot(); + for (stmt, (scope_id, parents), visibility) in deferred_functions { let parents_snapshot = parents.len(); - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; self.ctx.visible_scope = visibility; @@ -4830,13 +4802,10 @@ impl<'a> Checker<'a> { } } - let mut scopes = std::mem::take(&mut self.ctx.scope_stack); - scopes.restore(scope_snapshot); - let mut parents = std::mem::take(&mut self.ctx.parents); parents.truncate(parents_snapshot); - self.deferred.assignments.push((scopes, parents)); + self.deferred.assignments.push((scope_id, parents)); } } } @@ -4844,11 +4813,10 @@ impl<'a> Checker<'a> { fn check_deferred_lambdas(&mut self) { while !self.deferred.lambdas.is_empty() { let lambdas = std::mem::take(&mut self.deferred.lambdas); - for (expr, (scopes, parents)) in lambdas { - let scope_snapshot = scopes.snapshot(); + for (expr, (scope_id, parents)) in lambdas { let parents_snapshot = parents.len(); - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; if let ExprKind::Lambda { args, body } = &expr.node { @@ -4858,12 +4826,9 @@ impl<'a> Checker<'a> { unreachable!("Expected ExprKind::Lambda"); } - let mut scopes = std::mem::take(&mut self.ctx.scope_stack); - scopes.restore(scope_snapshot); - let mut parents = std::mem::take(&mut self.ctx.parents); parents.truncate(parents_snapshot); - self.deferred.assignments.push((scopes, parents)); + self.deferred.assignments.push((scope_id, parents)); } } } @@ -4871,17 +4836,13 @@ impl<'a> Checker<'a> { fn check_deferred_assignments(&mut self) { while !self.deferred.assignments.is_empty() { let assignments = std::mem::take(&mut self.deferred.assignments); - for (scopes, ..) in assignments { - let mut scopes_iter = scopes.iter(); - let scope_index = *scopes_iter.next().unwrap(); - let parent_scope_index = *scopes_iter.next().unwrap(); - + for (scope_id, ..) in assignments { // pyflakes if self.settings.rules.enabled(Rule::UnusedVariable) { - pyflakes::rules::unused_variable(self, scope_index); + pyflakes::rules::unused_variable(self, scope_id); } if self.settings.rules.enabled(Rule::UnusedAnnotation) { - pyflakes::rules::unused_annotation(self, scope_index); + pyflakes::rules::unused_annotation(self, scope_id); } if !self.is_stub { @@ -4893,11 +4854,13 @@ impl<'a> Checker<'a> { Rule::UnusedStaticMethodArgument, Rule::UnusedLambdaArgument, ]) { + let scope = &self.ctx.scopes[scope_id]; + let parent = &self.ctx.scopes[scope.parent.unwrap()]; self.diagnostics .extend(flake8_unused_arguments::rules::unused_arguments( self, - &self.ctx.scopes[parent_scope_index], - &self.ctx.scopes[scope_index], + parent, + scope, &self.ctx.bindings, )); } @@ -4910,8 +4873,8 @@ impl<'a> Checker<'a> { while !self.deferred.for_loops.is_empty() { let for_loops = std::mem::take(&mut self.deferred.for_loops); - for (stmt, (scopes, parents)) in for_loops { - self.ctx.scope_stack = scopes; + for (stmt, (scope_id, parents)) in for_loops { + self.ctx.scope_id = scope_id; self.ctx.parents = parents; if let StmtKind::For { target, body, .. } @@ -5019,10 +4982,10 @@ impl<'a> Checker<'a> { }; let mut diagnostics: Vec = vec![]; - for (index, stack) in self.ctx.dead_scopes.iter().rev() { - let scope = &self.ctx.scopes[*index]; + for scope_id in self.ctx.dead_scopes.iter().rev() { + let scope = &self.ctx.scopes[*scope_id]; - if index.is_global() { + if scope.kind.is_module() { // F822 if self.settings.rules.enabled(Rule::UndefinedExport) { if !self.path.ends_with("__init__.py") { @@ -5148,11 +5111,10 @@ impl<'a> Checker<'a> { let runtime_imports: Vec<&Binding> = if self.settings.flake8_type_checking.strict { vec![] } else { - stack - .iter() - .rev() - .chain(iter::once(index)) - .flat_map(|index| runtime_imports[usize::from(*index)].iter()) + self.ctx + .scopes + .ancestor_ids(*scope_id) + .flat_map(|scope_id| runtime_imports[usize::from(scope_id)].iter()) .copied() .collect() }; @@ -5405,8 +5367,8 @@ impl<'a> Checker<'a> { let mut overloaded_name: Option = None; while !self.deferred.definitions.is_empty() { let definitions = std::mem::take(&mut self.deferred.definitions); - for (definition, visibility, (scopes, parents)) in definitions { - self.ctx.scope_stack = scopes; + for (definition, visibility, (scope_id, parents)) in definitions { + self.ctx.scope_id = scope_id; self.ctx.parents = parents; // flake8-annotations @@ -5677,8 +5639,8 @@ pub fn check_ast( checker.check_definitions(); // Reset the scope to module-level, and check all consumed scopes. - checker.ctx.scope_stack = ScopeStack::default(); - checker.ctx.pop_scope(); + checker.ctx.scope_id = ScopeId::global(); + checker.ctx.dead_scopes.push(ScopeId::global()); checker.check_dead_scopes(); checker.diagnostics diff --git a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs index 43f11928bf6a3..262c7163de36d 100644 --- a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs @@ -17,14 +17,12 @@ impl Violation for ReturnOutsideFunction { } pub fn return_outside_function(checker: &mut Checker, stmt: &Stmt) { - if let Some(index) = checker.ctx.scope_stack.top() { - if matches!( - checker.ctx.scopes[index].kind, - ScopeKind::Class(_) | ScopeKind::Module - ) { - checker - .diagnostics - .push(Diagnostic::new(ReturnOutsideFunction, stmt.range())); - } + if matches!( + checker.ctx.scope().kind, + ScopeKind::Class(_) | ScopeKind::Module + ) { + checker + .diagnostics + .push(Diagnostic::new(ReturnOutsideFunction, stmt.range())); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs index 456fd3841ab67..d489c83ed5381 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs @@ -2,8 +2,8 @@ use std::string::ToString; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::Bindings; -use ruff_python_semantic::scope::{Scope, ScopeKind}; + +use crate::checkers::ast::Checker; #[violation] pub struct UndefinedLocal { @@ -18,26 +18,39 @@ impl Violation for UndefinedLocal { } } -/// F821 -pub fn undefined_local(name: &str, scopes: &[&Scope], bindings: &Bindings) -> Option { - let current = &scopes.last().expect("No current scope found"); - if matches!(current.kind, ScopeKind::Function(_)) && !current.defines(name) { - for scope in scopes.iter().rev().skip(1) { - if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) { - if let Some(binding) = scope.get(name).map(|index| &bindings[*index]) { - if let Some((scope_id, location)) = binding.runtime_usage { - if scope_id == current.id { - return Some(Diagnostic::new( - UndefinedLocal { - name: name.to_string(), - }, - location, - )); - } - } +/// F823 +pub fn undefined_local(checker: &mut Checker, name: &str) { + // If the name hasn't already been defined in the current scope... + let current = checker.ctx.scope(); + if !current.kind.is_function() || current.defines(name) { + return; + } + + let Some(parent) = current.parent else { + return; + }; + + // For every function and module scope above us... + for scope in checker.ctx.scopes.ancestors(parent) { + if !(scope.kind.is_function() || scope.kind.is_module()) { + continue; + } + + // If the name was defined in that scope... + if let Some(binding) = scope.get(name).map(|index| &checker.ctx.bindings[*index]) { + // And has already been accessed in the current scope... + if let Some((scope_id, location)) = binding.runtime_usage { + if scope_id == checker.ctx.scope_id { + // Then it's probably an error. + checker.diagnostics.push(Diagnostic::new( + UndefinedLocal { + name: name.to_string(), + }, + location, + )); + return; } } } } - None } diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index b33965b1296b6..ca3e84daa6f7d 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -16,7 +16,7 @@ pub fn in_dunder_init(checker: &Checker) -> bool { if name != "__init__" { return false; } - let Some(parent) = checker.ctx.parent_scope() else { + let Some(parent) = scope.parent.map(|scope_id| &checker.ctx.scopes[scope_id]) else { return false; }; diff --git a/crates/ruff_python_semantic/src/context.rs b/crates/ruff_python_semantic/src/context.rs index f4eb2f2b2ca17..661bd7a88bbfe 100644 --- a/crates/ruff_python_semantic/src/context.rs +++ b/crates/ruff_python_semantic/src/context.rs @@ -1,23 +1,23 @@ use std::path::Path; use nohash_hasher::{BuildNoHashHasher, IntMap}; -use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath}; -use ruff_python_ast::helpers::from_relative_import; -use ruff_python_ast::types::RefEquality; -use ruff_python_ast::typing::AnnotationKind; use rustc_hash::FxHashMap; use rustpython_parser::ast::{Expr, Stmt}; use smallvec::smallvec; -use crate::analyze::visibility::{module_visibility, Modifier, VisibleScope}; +use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath}; +use ruff_python_ast::helpers::from_relative_import; +use ruff_python_ast::types::RefEquality; +use ruff_python_ast::typing::AnnotationKind; use ruff_python_stdlib::path::is_python_stub_file; use ruff_python_stdlib::typing::TYPING_EXTENSIONS; +use crate::analyze::visibility::{module_visibility, Modifier, VisibleScope}; use crate::binding::{ Binding, BindingId, BindingKind, Bindings, Exceptions, ExecutionContext, FromImportation, Importation, SubmoduleImportation, }; -use crate::scope::{Scope, ScopeId, ScopeKind, ScopeStack, Scopes}; +use crate::scope::{Scope, ScopeId, ScopeKind, Scopes}; #[allow(clippy::struct_excessive_bools)] pub struct Context<'a> { @@ -35,8 +35,8 @@ pub struct Context<'a> { std::collections::HashMap, BuildNoHashHasher>, pub exprs: Vec>, pub scopes: Scopes<'a>, - pub scope_stack: ScopeStack, - pub dead_scopes: Vec<(ScopeId, ScopeStack)>, + pub scope_id: ScopeId, + pub dead_scopes: Vec, // Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, @@ -75,7 +75,7 @@ impl<'a> Context<'a> { shadowed_bindings: IntMap::default(), exprs: Vec::default(), scopes: Scopes::default(), - scope_stack: ScopeStack::default(), + scope_id: ScopeId::global(), dead_scopes: Vec::default(), body: &[], body_index: 0, @@ -330,19 +330,18 @@ impl<'a> Context<'a> { .expect("Attempted to pop without expression"); } - pub fn push_scope(&mut self, kind: ScopeKind<'a>) -> ScopeId { - let id = self.scopes.push_scope(kind); - self.scope_stack.push(id); - id + /// Push a [`Scope`] with the given [`ScopeKind`] onto the stack. + pub fn push_scope(&mut self, kind: ScopeKind<'a>) { + let id = self.scopes.push_scope(kind, self.scope_id); + self.scope_id = id; } + /// Pop the current [`Scope`] off the stack. pub fn pop_scope(&mut self) { - self.dead_scopes.push(( - self.scope_stack - .pop() - .expect("Attempted to pop without scope"), - self.scope_stack.clone(), - )); + self.dead_scopes.push(self.scope_id); + self.scope_id = self.scopes[self.scope_id] + .parent + .expect("Attempted to pop without scope"); } /// Return the current `Stmt`. @@ -387,31 +386,20 @@ impl<'a> Context<'a> { /// Returns the current top most scope. pub fn scope(&self) -> &Scope<'a> { - &self.scopes[self.scope_stack.top().expect("No current scope found")] - } - - /// Returns the id of the top-most scope - pub fn scope_id(&self) -> ScopeId { - self.scope_stack.top().expect("No current scope found") + &self.scopes[self.scope_id] } /// Returns a mutable reference to the current top most scope. pub fn scope_mut(&mut self) -> &mut Scope<'a> { - let top_id = self.scope_stack.top().expect("No current scope found"); - &mut self.scopes[top_id] - } - - pub fn parent_scope(&self) -> Option<&Scope> { - self.scope_stack - .iter() - .nth(1) - .map(|index| &self.scopes[*index]) + &mut self.scopes[self.scope_id] } + /// Returns an iterator over all scopes, starting from the current scope. pub fn scopes(&self) -> impl Iterator { - self.scope_stack.iter().map(|index| &self.scopes[*index]) + self.scopes.ancestors(self.scope_id) } + /// Returns `true` if the context is in an exception handler. pub const fn in_exception_handler(&self) -> bool { self.in_exception_handler } diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 075148977b7ce..e1366de65aaa1 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -8,8 +8,9 @@ use crate::binding::{BindingId, StarImportation}; #[derive(Debug)] pub struct Scope<'a> { - pub id: ScopeId, pub kind: ScopeKind<'a>, + pub parent: Option, + /// Whether this scope uses the `locals()` builtin. pub uses_locals: bool, /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). @@ -22,13 +23,20 @@ pub struct Scope<'a> { impl<'a> Scope<'a> { pub fn global() -> Self { - Scope::local(ScopeId::global(), ScopeKind::Module) + Scope { + kind: ScopeKind::Module, + parent: None, + uses_locals: false, + star_imports: Vec::default(), + bindings: FxHashMap::default(), + shadowed_bindings: FxHashMap::default(), + } } - pub fn local(id: ScopeId, kind: ScopeKind<'a>) -> Self { + pub fn local(kind: ScopeKind<'a>, parent: ScopeId) -> Self { Scope { - id, kind, + parent: Some(parent), uses_locals: false, star_imports: Vec::default(), bindings: FxHashMap::default(), @@ -189,11 +197,23 @@ impl<'a> Scopes<'a> { } /// Pushes a new scope and returns its unique id - pub fn push_scope(&mut self, kind: ScopeKind<'a>) -> ScopeId { + pub fn push_scope(&mut self, kind: ScopeKind<'a>, parent: ScopeId) -> ScopeId { let next_id = ScopeId::try_from(self.0.len()).unwrap(); - self.0.push(Scope::local(next_id, kind)); + self.0.push(Scope::local(kind, parent)); next_id } + + /// Returns an iterator over all [`ScopeId`] ancestors, starting from the given [`ScopeId`]. + pub fn ancestor_ids(&self, scope_id: ScopeId) -> impl Iterator + '_ { + std::iter::successors(Some(scope_id), |&scope_id| self[scope_id].parent) + } + + /// Returns an iterator over all [`Scope`] ancestors, starting from the given [`ScopeId`]. + pub fn ancestors(&self, scope_id: ScopeId) -> impl Iterator + '_ { + std::iter::successors(Some(&self[scope_id]), |&scope| { + scope.parent.map(|scope_id| &self[scope_id]) + }) + } } impl Default for Scopes<'_> { @@ -222,45 +242,3 @@ impl<'a> Deref for Scopes<'a> { &self.0 } } - -#[derive(Debug, Clone)] -pub struct ScopeStack(Vec); - -impl ScopeStack { - /// Pushes a new scope on the stack - pub fn push(&mut self, id: ScopeId) { - self.0.push(id); - } - - /// Pops the top most scope - pub fn pop(&mut self) -> Option { - self.0.pop() - } - - /// Returns the id of the top-most - pub fn top(&self) -> Option { - self.0.last().copied() - } - - /// Returns an iterator from the current scope to the top scope (reverse iterator) - pub fn iter(&self) -> std::iter::Rev> { - self.0.iter().rev() - } - - pub fn snapshot(&self) -> ScopeStackSnapshot { - ScopeStackSnapshot(self.0.len()) - } - - #[allow(clippy::needless_pass_by_value)] - pub fn restore(&mut self, snapshot: ScopeStackSnapshot) { - self.0.truncate(snapshot.0); - } -} - -pub struct ScopeStackSnapshot(usize); - -impl Default for ScopeStack { - fn default() -> Self { - Self(vec![ScopeId::global()]) - } -} From 8d64747d3453662370db904e5875fa04b06c80d4 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Sat, 29 Apr 2023 17:41:04 -0500 Subject: [PATCH 14/32] Remove `pyright` comment prefix from PYI033 checks (#4152) --- .../ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index c699875e22d7f..f2bc89699f199 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -50,7 +50,7 @@ pub fn type_comment_in_stub(tokens: &[LexResult]) -> Vec { } static TYPE_COMMENT_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^#\s*(type|pyright):\s*([^#]+)(\s*#.*?)?$").unwrap()); + Lazy::new(|| Regex::new(r"^#\s*type:\s*([^#]+)(\s*#.*?)?$").unwrap()); static TYPE_IGNORE_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^#\s*(type|pyright):\s*ignore([^#]+)?(\s*#.*?)?$").unwrap()); + Lazy::new(|| Regex::new(r"^#\s*type:\s*ignore([^#]+)?(\s*#.*?)?$").unwrap()); From 64b7280eb824d0e5f9da887e82dcda53838dd38d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 29 Apr 2023 18:45:30 -0400 Subject: [PATCH 15/32] Respect parent-scoping rules for `NamedExpr` assignments (#4145) --- .../test/fixtures/pyflakes/F821_0.py | 5 ++ .../test/fixtures/pyflakes/F841_0.py | 5 ++ crates/ruff/src/checkers/ast/mod.rs | 53 ++++++++++++++++--- .../rules/pyflakes/rules/unused_variable.rs | 2 +- ...ules__pyflakes__tests__F841_F841_0.py.snap | 9 ++++ ...lakes__tests__f841_dummy_variable_rgx.snap | 9 ++++ crates/ruff_python_semantic/src/binding.rs | 1 + 7 files changed, 76 insertions(+), 8 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F821_0.py b/crates/ruff/resources/test/fixtures/pyflakes/F821_0.py index 03b55606388c4..82c098db1c8d8 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F821_0.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F821_0.py @@ -132,3 +132,8 @@ def in_ipython_notebook() -> bool: except NameError: return False # not in notebook return True + + +def named_expr(): + if any((key := (value := x)) for x in ["ok"]): + print(key) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F841_0.py b/crates/ruff/resources/test/fixtures/pyflakes/F841_0.py index 90c44f761c40c..c0e33502d6930 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F841_0.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F841_0.py @@ -121,3 +121,8 @@ def f(x: int): print("A") case y: pass + + +def f(): + if any((key := (value := x)) for x in ["ok"]): + print(key) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 8594d58dd8908..7b348e1488e7a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4266,7 +4266,20 @@ impl<'a> Checker<'a> { } } - let scope = self.ctx.scope(); + // Per [PEP 572](https://peps.python.org/pep-0572/#scope-of-the-target), named + // expressions in generators and comprehensions bind to the scope that contains the + // outermost comprehension. + let scope_id = if binding.kind.is_named_expr_assignment() { + self.ctx + .scopes + .ancestor_ids(self.ctx.scope_id) + .find_or_last(|scope_id| !self.ctx.scopes[*scope_id].kind.is_generator()) + .unwrap_or(self.ctx.scope_id) + } else { + self.ctx.scope_id + }; + let scope = &mut self.ctx.scopes[scope_id]; + let binding = if let Some(index) = scope.get(name) { let existing = &self.ctx.bindings[*index]; match &existing.kind { @@ -4298,11 +4311,15 @@ impl<'a> Checker<'a> { // Don't treat annotations as assignments if there is an existing value // in scope. - let scope = self.ctx.scope_mut(); - if !(binding.kind.is_annotation() && scope.defines(name)) { - scope.add(name, binding_id); + if binding.kind.is_annotation() && scope.defines(name) { + self.ctx.bindings.push(binding); + return; } + // Add the binding to the scope. + scope.add(name, binding_id); + + // Add the binding to the arena. self.ctx.bindings.push(binding); } @@ -4579,9 +4596,10 @@ impl<'a> Checker<'a> { return; } - let current = self.ctx.scope(); + let scope = self.ctx.scope(); + if id == "__all__" - && matches!(current.kind, ScopeKind::Module) + && scope.kind.is_module() && matches!( parent.node, StmtKind::Assign { .. } | StmtKind::AugAssign { .. } | StmtKind::AnnAssign { .. } @@ -4619,7 +4637,7 @@ impl<'a> Checker<'a> { // Grab the existing bound __all__ values. if let StmtKind::AugAssign { .. } = &parent.node { - if let Some(index) = current.get("__all__") { + if let Some(index) = scope.get("__all__") { if let BindingKind::Export(Export { names: existing }) = &self.ctx.bindings[*index].kind { @@ -4662,6 +4680,27 @@ impl<'a> Checker<'a> { } } + if self + .ctx + .expr_ancestors() + .any(|expr| matches!(expr.node, ExprKind::NamedExpr { .. })) + { + self.add_binding( + id, + Binding { + kind: BindingKind::NamedExprAssignment, + runtime_usage: None, + synthetic_usage: None, + typing_usage: None, + range: expr.range(), + source: Some(*self.ctx.current_stmt()), + context: self.ctx.execution_context(), + exceptions: self.ctx.exceptions(), + }, + ); + return; + } + self.add_binding( id, Binding { diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index c4f0024f65b4f..2868b5112e95a 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -322,7 +322,7 @@ pub fn unused_variable(checker: &mut Checker, scope: ScopeId) { .map(|(name, index)| (name, &checker.ctx.bindings[*index])) { if !binding.used() - && binding.kind.is_assignment() + && (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment()) && !checker.settings.dummy_variable_rgx.is_match(name) && name != &"__tracebackhide__" && name != &"__traceback_info__" diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap index a3ed914c1e7cc..95da0702b47c9 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap @@ -210,4 +210,13 @@ F841_0.py:122:14: F841 [*] Local variable `y` is assigned to but never used | = help: Remove assignment to unused variable `y` +F841_0.py:127:21: F841 [*] Local variable `value` is assigned to but never used + | +127 | def f(): +128 | if any((key := (value := x)) for x in ["ok"]): + | ^^^^^ F841 +129 | print(key) + | + = help: Remove assignment to unused variable `value` + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap index cb7da80550a98..55ad23a7604d7 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap @@ -248,4 +248,13 @@ F841_0.py:122:14: F841 [*] Local variable `y` is assigned to but never used | = help: Remove assignment to unused variable `y` +F841_0.py:127:21: F841 [*] Local variable `value` is assigned to but never used + | +127 | def f(): +128 | if any((key := (value := x)) for x in ["ok"]): + | ^^^^^ F841 +129 | print(key) + | + = help: Remove assignment to unused variable `value` + diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index f531ff4f792be..9c50cfd132a1c 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -253,6 +253,7 @@ pub enum BindingKind<'a> { Annotation, Argument, Assignment, + NamedExprAssignment, Binding, LoopVar, Global, From a32617911a36998d2ad88ee7bafcd7c9e01720f1 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sun, 30 Apr 2023 13:39:22 +0200 Subject: [PATCH 16/32] Use --filter=blob:none to clone CPython faster (#4156) --- scripts/benchmarks/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/benchmarks/setup.sh b/scripts/benchmarks/setup.sh index 209d1af547eb9..7024ab9a7da24 100755 --- a/scripts/benchmarks/setup.sh +++ b/scripts/benchmarks/setup.sh @@ -4,4 +4,4 @@ # Setup the CPython repository to enable benchmarking. ### -git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resources/test/cpython +git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resources/test/cpython --filter=blob:none From 8c97e7922bcfa489d29bb4158466dfacea252f35 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sun, 30 Apr 2023 20:39:45 +0200 Subject: [PATCH 17/32] Fix F811 false positive with match (#4161) --- .../resources/test/fixtures/pyflakes/F811_22.py | 13 +++++++++++++ crates/ruff/src/rules/pyflakes/mod.rs | 1 + ...ff__rules__pyflakes__tests__F811_F811_22.py.snap | 4 ++++ crates/ruff_python_ast/src/branch_detection.rs | 4 ++++ 4 files changed, 22 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/pyflakes/F811_22.py create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_22.py.snap diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F811_22.py b/crates/ruff/resources/test/fixtures/pyflakes/F811_22.py new file mode 100644 index 0000000000000..741e7554e84f6 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyflakes/F811_22.py @@ -0,0 +1,13 @@ +def redef(value): + match value: + case True: + + def fun(x, y): + return x + + case False: + + def fun(x, y): + return y + + return fun \ No newline at end of file diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 6df5ea86f3902..a6b0196adb9f7 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -96,6 +96,7 @@ mod tests { #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_19.py"); "F811_19")] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_20.py"); "F811_20")] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_21.py"); "F811_21")] + #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_22.py"); "F811_22")] #[test_case(Rule::UndefinedName, Path::new("F821_0.py"); "F821_0")] #[test_case(Rule::UndefinedName, Path::new("F821_1.py"); "F821_1")] #[test_case(Rule::UndefinedName, Path::new("F821_2.py"); "F821_2")] diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_22.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_22.py.snap new file mode 100644 index 0000000000000..1976c4331d419 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_22.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff_python_ast/src/branch_detection.rs b/crates/ruff_python_ast/src/branch_detection.rs index 42850452d9777..2e3b2ced285d4 100644 --- a/crates/ruff_python_ast/src/branch_detection.rs +++ b/crates/ruff_python_ast/src/branch_detection.rs @@ -71,6 +71,10 @@ fn alternatives(stmt: RefEquality) -> Vec>> { body.iter().map(RefEquality).collect() })) .collect(), + StmtKind::Match { cases, .. } => cases + .iter() + .map(|case| case.body.iter().map(RefEquality).collect()) + .collect(), _ => vec![], } } From 814731364afa995ae4a65da5e0dd85791a64b4c2 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:57:41 +0200 Subject: [PATCH 18/32] Fix UP032 auto-fix (#4165) --- .../test/fixtures/pyupgrade/UP032.py | 8 + .../src/rules/pyupgrade/rules/f_strings.rs | 7 + ...ff__rules__pyupgrade__tests__UP032.py.snap | 160 +++++++++++++----- 3 files changed, 137 insertions(+), 38 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP032.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP032.py index 173d986f05507..c45821bc96cdd 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP032.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP032.py @@ -46,6 +46,14 @@ '({}={{0!e}})'.format(a) +"{[b]}".format(a) + +'{[b]}'.format(a) + +"""{[b]}""".format(a) + +'''{[b]}'''.format(a) + ### # Non-errors ### diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index 6415e6744cc10..2038ef5bf7101 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -200,8 +200,15 @@ fn try_convert_to_f_string(checker: &Checker, expr: &Expr) -> Option { converted.push(']'); } FieldNamePart::StringIndex(index) => { + let quote = match *trailing_quote { + "'" | "'''" | "\"\"\"" => '"', + "\"" => '\'', + _ => unreachable!("invalid trailing quote"), + }; converted.push('['); + converted.push(quote); converted.push_str(&index); + converted.push(quote); converted.push(']'); } } diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032.py.snap index fca6f04d93e89..2560a9f4ce235 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032.py.snap @@ -374,7 +374,7 @@ UP032.py:39:1: UP032 [*] Use f-string instead of `format` call 37 37 | print("foo {} ".format(x)) 38 38 | 39 |-"{a[b]}".format(a=a) - 39 |+f"{a[b]}" + 39 |+f"{a['b']}" 40 40 | 41 41 | "{a.a[b]}".format(a=a) 42 42 | @@ -395,7 +395,7 @@ UP032.py:41:1: UP032 [*] Use f-string instead of `format` call 39 39 | "{a[b]}".format(a=a) 40 40 | 41 |-"{a.a[b]}".format(a=a) - 41 |+f"{a.a[b]}" + 41 |+f"{a.a['b']}" 42 42 | 43 43 | "{}{{}}{}".format(escaped, y) 44 44 | @@ -449,7 +449,7 @@ UP032.py:47:1: UP032 [*] Use f-string instead of `format` call 49 | '({}={{0!e}})'.format(a) | ^^^^^^^^^^^^^^^^^^^^^^^^ UP032 50 | -51 | ### +51 | "{[b]}".format(a) | = help: Convert to f-string @@ -460,57 +460,141 @@ UP032.py:47:1: UP032 [*] Use f-string instead of `format` call 47 |-'({}={{0!e}})'.format(a) 47 |+f'({a}={{0!e}})' 48 48 | -49 49 | ### -50 50 | # Non-errors +49 49 | "{[b]}".format(a) +50 50 | -UP032.py:92:11: UP032 [*] Use f-string instead of `format` call +UP032.py:49:1: UP032 [*] Use f-string instead of `format` call | -92 | def d(osname, version, release): -93 | return"{}-{}.{}".format(osname, version, release) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032 +49 | '({}={{0!e}})'.format(a) +50 | +51 | "{[b]}".format(a) + | ^^^^^^^^^^^^^^^^^ UP032 +52 | +53 | '{[b]}'.format(a) | = help: Convert to f-string ℹ Suggested fix -89 89 | -90 90 | -91 91 | def d(osname, version, release): -92 |- return"{}-{}.{}".format(osname, version, release) - 92 |+ return f"{osname}-{version}.{release}" -93 93 | -94 94 | -95 95 | def e(): +46 46 | +47 47 | '({}={{0!e}})'.format(a) +48 48 | +49 |-"{[b]}".format(a) + 49 |+f"{a['b']}" +50 50 | +51 51 | '{[b]}'.format(a) +52 52 | -UP032.py:96:10: UP032 [*] Use f-string instead of `format` call +UP032.py:51:1: UP032 [*] Use f-string instead of `format` call | -96 | def e(): -97 | yield"{}".format(1) - | ^^^^^^^^^^^^^^ UP032 +51 | "{[b]}".format(a) +52 | +53 | '{[b]}'.format(a) + | ^^^^^^^^^^^^^^^^^ UP032 +54 | +55 | """{[b]}""".format(a) | = help: Convert to f-string ℹ Suggested fix -93 93 | -94 94 | -95 95 | def e(): -96 |- yield"{}".format(1) - 96 |+ yield f"{1}" -97 97 | -98 98 | -99 99 | assert"{}".format(1) - -UP032.py:99:7: UP032 [*] Use f-string instead of `format` call +48 48 | +49 49 | "{[b]}".format(a) +50 50 | +51 |-'{[b]}'.format(a) + 51 |+f'{a["b"]}' +52 52 | +53 53 | """{[b]}""".format(a) +54 54 | + +UP032.py:53:1: UP032 [*] Use f-string instead of `format` call + | +53 | '{[b]}'.format(a) +54 | +55 | """{[b]}""".format(a) + | ^^^^^^^^^^^^^^^^^^^^^ UP032 +56 | +57 | '''{[b]}'''.format(a) | -99 | assert"{}".format(1) - | ^^^^^^^^^^^^^^ UP032 + = help: Convert to f-string + +ℹ Suggested fix +50 50 | +51 51 | '{[b]}'.format(a) +52 52 | +53 |-"""{[b]}""".format(a) + 53 |+f"""{a["b"]}""" +54 54 | +55 55 | '''{[b]}'''.format(a) +56 56 | + +UP032.py:55:1: UP032 [*] Use f-string instead of `format` call + | +55 | """{[b]}""".format(a) +56 | +57 | '''{[b]}'''.format(a) + | ^^^^^^^^^^^^^^^^^^^^^ UP032 +58 | +59 | ### | = help: Convert to f-string ℹ Suggested fix -96 96 | yield"{}".format(1) -97 97 | -98 98 | -99 |-assert"{}".format(1) - 99 |+assert f"{1}" +52 52 | +53 53 | """{[b]}""".format(a) +54 54 | +55 |-'''{[b]}'''.format(a) + 55 |+f'''{a["b"]}''' +56 56 | +57 57 | ### +58 58 | # Non-errors + +UP032.py:100:11: UP032 [*] Use f-string instead of `format` call + | +100 | def d(osname, version, release): +101 | return"{}-{}.{}".format(osname, version, release) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032 + | + = help: Convert to f-string + +ℹ Suggested fix +97 97 | +98 98 | +99 99 | def d(osname, version, release): +100 |- return"{}-{}.{}".format(osname, version, release) + 100 |+ return f"{osname}-{version}.{release}" +101 101 | +102 102 | +103 103 | def e(): + +UP032.py:104:10: UP032 [*] Use f-string instead of `format` call + | +104 | def e(): +105 | yield"{}".format(1) + | ^^^^^^^^^^^^^^ UP032 + | + = help: Convert to f-string + +ℹ Suggested fix +101 101 | +102 102 | +103 103 | def e(): +104 |- yield"{}".format(1) + 104 |+ yield f"{1}" +105 105 | +106 106 | +107 107 | assert"{}".format(1) + +UP032.py:107:7: UP032 [*] Use f-string instead of `format` call + | +107 | assert"{}".format(1) + | ^^^^^^^^^^^^^^ UP032 + | + = help: Convert to f-string + +ℹ Suggested fix +104 104 | yield"{}".format(1) +105 105 | +106 106 | +107 |-assert"{}".format(1) + 107 |+assert f"{1}" From 2d6d51f3a11e5194939c9cb6db629cc3cf782f43 Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Tue, 2 May 2023 01:53:46 +0100 Subject: [PATCH 19/32] Add `flake8-return` docs (#4164) --- crates/ruff/src/rules/flake8_return/rules.rs | 203 +++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/crates/ruff/src/rules/flake8_return/rules.rs b/crates/ruff/src/rules/flake8_return/rules.rs index 6737a98ef19f8..88e8e6694c54a 100644 --- a/crates/ruff/src/rules/flake8_return/rules.rs +++ b/crates/ruff/src/rules/flake8_return/rules.rs @@ -19,6 +19,31 @@ use super::branch::Branch; use super::helpers::result_exists; use super::visitor::{ReturnVisitor, Stack}; +/// ## What it does +/// Checks for the presence of a `return None` statement when `None` is the only +/// possible return value. +/// +/// ## Why is this bad? +/// Python implicitly assumes `return None` if an explicit `return` value is +/// omitted. Therefore, explicitly returning `None` is redundant and should be +/// avoided when it is the only possible `return` value across all code paths +/// in a given function. +/// +/// ## Example +/// ```python +/// def foo(bar): +/// if not bar: +/// return +/// return None +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar): +/// if not bar: +/// return +/// return +/// ``` #[violation] pub struct UnnecessaryReturnNone; @@ -35,6 +60,32 @@ impl AlwaysAutofixableViolation for UnnecessaryReturnNone { } } +/// ## What it does +/// Checks for the presence of a `return` statement with no explicit value, +/// for functions that return non-`None` values elsewhere. +/// +/// ## Why is this bad? +/// Including a `return` statement with no explicit value can cause confusion +/// when other `return` statements in the function return non-`None` values. +/// Python implicitly assumes return `None` if no other return value is present. +/// Adding an explicit `return None` can make the code more readable by clarifying +/// intent. +/// +/// ## Example +/// ```python +/// def foo(bar): +/// if not bar: +/// return +/// return 1 +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar): +/// if not bar: +/// return None +/// return 1 +/// ``` #[violation] pub struct ImplicitReturnValue; @@ -49,6 +100,30 @@ impl AlwaysAutofixableViolation for ImplicitReturnValue { } } +/// ## What it does +/// Checks for missing explicit `return` statements at the end of functions +/// that can return non-`None` values. +/// +/// ## Why is this bad? +/// The lack of an explicit `return` statement at the end of a function that +/// can return non-`None` values can cause confusion. Python implicitly returns +/// `None` if no other return value is present. Adding an explicit +/// `return None` can make the code more readable by clarifying intent. +/// +/// ## Example +/// ```python +/// def foo(bar): +/// if not bar: +/// return 1 +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar): +/// if not bar: +/// return 1 +/// return None +/// ``` #[violation] pub struct ImplicitReturn; @@ -63,6 +138,30 @@ impl AlwaysAutofixableViolation for ImplicitReturn { } } +/// ## What it does +/// Checks for variable assignments that are unused between the assignment and +/// a `return` of the variable. +/// +/// ## Why is this bad? +/// The variable assignment is not necessary as the value can be returned +/// directly. +/// +/// ## Example +/// ```python +/// def foo(): +/// bar = 1 +/// # some code that not using `bar` +/// print('test') +/// return bar +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(): +/// # some code that not using `bar` +/// print('test') +/// return 1 +/// ``` #[violation] pub struct UnnecessaryAssign; @@ -73,6 +172,31 @@ impl Violation for UnnecessaryAssign { } } +/// ## What it does +/// Checks for `else` statements with a `return` statement in the preceding +/// `if` block. +/// +/// ## Why is this bad? +/// The `else` statement is not needed as the `return` statement will always +/// break out of the enclosing function. Removing the `else` will reduce +/// nesting and make the code more readable. +/// +/// ## Example +/// ```python +/// def foo(bar, baz): +/// if bar: +/// return 1 +/// else: +/// return baz +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar, baz): +/// if bar: +/// return 1 +/// return baz +/// ``` #[violation] pub struct SuperfluousElseReturn { pub branch: Branch, @@ -86,6 +210,31 @@ impl Violation for SuperfluousElseReturn { } } +/// ## What it does +/// Checks for `else` statements with a `raise` statement in the preceding `if` +/// block. +/// +/// ## Why is this bad? +/// The `else` statement is not needed as the `raise` statement will always +/// break out of the current scope. Removing the `else` will reduce nesting +/// and make the code more readable. +/// +/// ## Example +/// ```python +/// def foo(bar, baz): +/// if bar == "Specific Error": +/// raise Exception(bar) +/// else: +/// raise Exception(baz) +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar, baz): +/// if bar == "Specific Error": +/// raise Exception(bar) +/// raise Exception(baz) +/// ``` #[violation] pub struct SuperfluousElseRaise { pub branch: Branch, @@ -99,6 +248,33 @@ impl Violation for SuperfluousElseRaise { } } +/// ## What it does +/// Checks for `else` statements with a `continue` statement in the preceding +/// `if` block. +/// +/// ## Why is this bad? +/// The `else` statement is not needed, as the `continue` statement will always +/// continue onto the next iteration of a loop. Removing the `else` will reduce +/// nesting and make the code more readable. +/// +/// ## Example +/// ```python +///def foo(bar, baz): +/// for i in bar: +/// if i < baz: +/// continue +/// else: +/// x = 0 +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar, baz): +/// for i in bar: +/// if i < baz: +/// continue +/// x = 0 +/// ``` #[violation] pub struct SuperfluousElseContinue { pub branch: Branch, @@ -112,6 +288,33 @@ impl Violation for SuperfluousElseContinue { } } +/// ## What it does +/// Checks for `else` statements with a `break` statement in the preceding `if` +/// block. +/// +/// ## Why is this bad? +/// The `else` statement is not needed, as the `break` statement will always +/// break out of the loop. Removing the `else` will reduce nesting and make the +/// code more readable. +/// +/// ## Example +/// ```python +/// def foo(bar, baz): +/// for i in bar: +/// if i > baz: +/// break +/// else: +/// x = 0 +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(bar, baz): +/// for i in bar: +/// if i > baz: +/// break +/// x = 0 +/// ``` #[violation] pub struct SuperfluousElseBreak { pub branch: Branch, From a4ce7468925f28629d5e7b8ee73e26f0fed91803 Mon Sep 17 00:00:00 2001 From: Calum Young <32770960+calumy@users.noreply.github.com> Date: Tue, 2 May 2023 01:59:00 +0100 Subject: [PATCH 20/32] Reference related settings in rules (#4157) --- crates/ruff_dev/src/generate_rules_table.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 7b489a8543ae9..813b472080bf6 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -1,9 +1,11 @@ //! Generate a Markdown-compatible table of supported lint rules. use itertools::Itertools; +use strum::IntoEnumIterator; + use ruff::registry::{Linter, Rule, RuleNamespace, UpstreamCategory}; +use ruff::settings::options::Options; use ruff_diagnostics::AutofixKind; -use strum::IntoEnumIterator; const FIX_SYMBOL: &str = "🛠"; @@ -78,6 +80,19 @@ pub fn generate() -> String { table_out.push('\n'); } + if Options::metadata() + .iter() + .any(|(name, _)| name == &linter.name()) + { + table_out.push_str(&format!( + "For related settings, see [{}](settings.md#{}).", + linter.name(), + linter.name(), + )); + table_out.push('\n'); + table_out.push('\n'); + } + if let Some(categories) = linter.upstream_categories() { for UpstreamCategory(prefix, name) in categories { table_out.push_str(&format!( From 56c45013c2436186ce292b310022e387ef0ea341 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 1 May 2023 18:07:50 -0700 Subject: [PATCH 21/32] Allow boolean parameters for `pytest.param` (#4176) --- crates/ruff/src/rules/flake8_boolean_trap/rules.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules.rs index bf6a788223b91..f09ece952f205 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules.rs @@ -42,22 +42,23 @@ const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[ "assertEquals", "assertNotEqual", "assertNotEquals", + "bytes", "failIfEqual", "failUnlessEqual", + "float", "fromkeys", "get", "getattr", + "getboolean", + "getfloat", + "getint", "index", + "int", + "param", "pop", "setattr", "setdefault", "str", - "bytes", - "int", - "float", - "getint", - "getfloat", - "getboolean", ]; const FUNC_DEF_NAME_ALLOWLIST: &[&str] = &["__setitem__"]; From 8cb76f85eba1c970a8c800348fd1e0c874621a57 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 1 May 2023 23:33:38 -0700 Subject: [PATCH 22/32] Bump version to 0.0.264 (#4179) --- Cargo.lock | 6 +++--- README.md | 2 +- crates/flake8_to_ruff/Cargo.toml | 2 +- crates/ruff/Cargo.toml | 2 +- crates/ruff_cli/Cargo.toml | 2 +- docs/tutorial.md | 2 +- docs/usage.md | 4 ++-- pyproject.toml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43631701f4a98..7613d1b372250 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,7 +841,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flake8-to-ruff" -version = "0.0.263" +version = "0.0.264" dependencies = [ "anyhow", "clap 4.2.4", @@ -2004,7 +2004,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.263" +version = "0.0.264" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -2093,7 +2093,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.263" +version = "0.0.264" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index b9674e0e29f96..2aac48948fced 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.263' + rev: 'v0.0.264' hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index 06da684e7ed3c..96295994a3a9e 100644 --- a/crates/flake8_to_ruff/Cargo.toml +++ b/crates/flake8_to_ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flake8-to-ruff" -version = "0.0.263" +version = "0.0.264" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 3fc2629da98e2..f845a24a8b749 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.263" +version = "0.0.264" authors.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 0edb930eaaca5..c47e55924b17c 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.263" +version = "0.0.264" authors = ["Charlie Marsh "] edition = { workspace = true } rust-version = { workspace = true } diff --git a/docs/tutorial.md b/docs/tutorial.md index bad96ddd0b7e3..fb9df2f619a99 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -242,7 +242,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.263' + rev: 'v0.0.264' hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 2ad6e82daa2af..8b8634ef674d2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -22,7 +22,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.263' + rev: 'v0.0.264' hooks: - id: ruff ``` @@ -32,7 +32,7 @@ Or, to enable autofix: ```yaml - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.263' + rev: 'v0.0.264' hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/pyproject.toml b/pyproject.toml index 917a8f8a9506f..6bd4387104dce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.263" +version = "0.0.264" description = "An extremely fast Python linter, written in Rust." authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] From ac600bb3da05b7259a8b646c36e1dfe21ef8c450 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Tue, 2 May 2023 07:49:20 +0100 Subject: [PATCH 23/32] Warn on PEP 604 syntax not in an annotation, but don't autofix (#4170) --- crates/ruff/src/checkers/ast/mod.rs | 3 +-- .../pyupgrade/rules/use_pep604_annotation.rs | 13 ++++++------ ...ff__rules__pyupgrade__tests__UP007.py.snap | 20 +++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 7b348e1488e7a..a41407746651c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2281,8 +2281,7 @@ where match &expr.node { ExprKind::Subscript { value, slice, .. } => { // Ex) Optional[...], Union[...] - if self.ctx.in_type_definition - && !self.settings.pyupgrade.keep_runtime_typing + if !self.settings.pyupgrade.keep_runtime_typing && self.settings.rules.enabled(Rule::NonPEP604Annotation) && (self.settings.target_version >= PythonVersion::Py310 || (self.settings.target_version >= PythonVersion::Py37 diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 2c51cb2d32828..e6e1500d37fa6 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -100,12 +100,13 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s return; }; - // Avoid fixing forward references. - let fixable = checker - .ctx - .in_deferred_string_type_definition - .as_ref() - .map_or(true, AnnotationKind::is_simple); + // Avoid fixing forward references, or types not in an annotation. + let fixable = checker.ctx.in_type_definition + && checker + .ctx + .in_deferred_string_type_definition + .as_ref() + .map_or(true, AnnotationKind::is_simple); match typing_member { TypingMember::Optional => { diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap index 079806c6bfb02..b844f6a84b223 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap @@ -200,6 +200,26 @@ UP007.py:47:8: UP007 [*] Use `X | Y` for type annotations 49 49 | 50 50 | x = Union[str, int] +UP007.py:48:9: UP007 Use `X | Y` for type annotations + | +48 | def f() -> None: +49 | x: Optional[str] +50 | x = Optional[str] + | ^^^^^^^^^^^^^ UP007 +51 | +52 | x = Union[str, int] + | + +UP007.py:50:9: UP007 Use `X | Y` for type annotations + | +50 | x = Optional[str] +51 | +52 | x = Union[str, int] + | ^^^^^^^^^^^^^^^ UP007 +53 | x = Union["str", "int"] +54 | x: Union[str, int] + | + UP007.py:52:8: UP007 [*] Use `X | Y` for type annotations | 52 | x = Union[str, int] From b14358fbfe3d59dead9cc3efcded6c0468f2bda3 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 2 May 2023 15:14:02 +0200 Subject: [PATCH 24/32] Render tabs as 4 spaces in diagnostics (#4132) --- crates/ruff/src/message/text.rs | 75 +++++- ...les__pycodestyle__tests__E101_E101.py.snap | 12 +- ...ules__pycodestyle__tests__E117_E11.py.snap | 4 +- ...ules__pycodestyle__tests__E201_E20.py.snap | 16 +- ...ules__pycodestyle__tests__E202_E20.py.snap | 16 +- ...ules__pycodestyle__tests__E203_E20.py.snap | 6 +- ...ules__pycodestyle__tests__E223_E22.py.snap | 2 +- ...ules__pycodestyle__tests__E224_E22.py.snap | 2 +- ...ules__pycodestyle__tests__E271_E27.py.snap | 4 +- ...ules__pycodestyle__tests__E272_E27.py.snap | 2 +- ...ules__pycodestyle__tests__E273_E27.py.snap | 24 +- ...ules__pycodestyle__tests__E274_E27.py.snap | 14 +- ...ules__pycodestyle__tests__W191_W19.py.snap | 228 +++++++++--------- 13 files changed, 229 insertions(+), 176 deletions(-) diff --git a/crates/ruff/src/message/text.rs b/crates/ruff/src/message/text.rs index c0801a3ce6f8d..547cb64e77a3c 100644 --- a/crates/ruff/src/message/text.rs +++ b/crates/ruff/src/message/text.rs @@ -8,7 +8,8 @@ use bitflags::bitflags; use colored::Colorize; use ruff_diagnostics::DiagnosticKind; use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; -use ruff_text_size::TextRange; +use ruff_text_size::{TextRange, TextSize}; +use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::io::Write; @@ -172,6 +173,7 @@ impl Display for MessageCodeFrame<'_> { }; let source_code = file.to_source_code(); + let content_start_index = source_code.line_index(range.start()); let mut start_index = content_start_index.saturating_sub(2); @@ -200,26 +202,23 @@ impl Display for MessageCodeFrame<'_> { let start_offset = source_code.line_start(start_index); let end_offset = source_code.line_end(end_index); - let source_text = source_code.slice(TextRange::new(start_offset, end_offset)); - - let annotation_start_offset = range.start() - start_offset; - let annotation_end_offset = range.end() - start_offset; + let source = replace_whitespace( + source_code.slice(TextRange::new(start_offset, end_offset)), + range - start_offset, + ); - let start_char = source_text[TextRange::up_to(annotation_start_offset)] + let start_char = source.text[TextRange::up_to(source.annotation_range.start())] .chars() .count(); - let char_length = source_text - [TextRange::new(annotation_start_offset, annotation_end_offset)] - .chars() - .count(); + let char_length = source.text[source.annotation_range].chars().count(); let label = kind.rule().noqa_code().to_string(); let snippet = Snippet { title: None, slices: vec![Slice { - source: source_text, + source: &source.text, line_start: content_start_index.get(), annotations: vec![SourceAnnotation { label: &label, @@ -245,6 +244,60 @@ impl Display for MessageCodeFrame<'_> { } } +fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode { + static TAB_SIZE: TextSize = TextSize::new(4); + + let mut result = String::new(); + let mut last_end = 0; + let mut range = annotation_range; + let mut column = 0; + + for (index, m) in source.match_indices(['\t', '\n', '\r']) { + match m { + "\t" => { + let tab_width = TAB_SIZE - TextSize::new(column % 4); + + if index < usize::from(annotation_range.start()) { + range += tab_width - TextSize::new(1); + } else if index < usize::from(annotation_range.end()) { + range = range.add_end(tab_width - TextSize::new(1)); + } + + result.push_str(&source[last_end..index]); + + for _ in 0..u32::from(tab_width) { + result.push(' '); + } + + last_end = index + 1; + } + "\n" | "\r" => { + column = 0; + } + _ => unreachable!(), + } + } + + // No tabs + if result.is_empty() { + SourceCode { + annotation_range, + text: Cow::Borrowed(source), + } + } else { + result.push_str(&source[last_end..]); + SourceCode { + annotation_range: range, + text: Cow::Owned(result), + } + } +} + +struct SourceCode<'a> { + text: Cow<'a, str>, + annotation_range: TextRange, +} + #[cfg(test)] mod tests { use crate::message::tests::{capture_emitter_output, create_messages}; diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E101_E101.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E101_E101.py.snap index 754f1c42a2a1d..d1b7d11aee8f3 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E101_E101.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E101_E101.py.snap @@ -5,8 +5,8 @@ E101.py:11:1: E101 Indentation contains mixed spaces and tabs | 11 | def func_mixed_start_with_tab(): 12 | # E101 -13 | print("mixed starts with tab") - | ^^ E101 +13 | print("mixed starts with tab") + | ^^^^^^ E101 14 | 15 | def func_mixed_start_with_space(): | @@ -15,8 +15,8 @@ E101.py:15:1: E101 Indentation contains mixed spaces and tabs | 15 | def func_mixed_start_with_space(): 16 | # E101 -17 | print("mixed starts with space") - | ^^^^^^^^ E101 +17 | print("mixed starts with space") + | ^^^^^^^^^^^^^^^^^^^^ E101 18 | 19 | def xyz(): | @@ -25,8 +25,8 @@ E101.py:19:1: E101 Indentation contains mixed spaces and tabs | 19 | def xyz(): 20 | # E101 -21 | print("xyz"); - | ^^^ E101 +21 | print("xyz"); + | ^^^^^^^ E101 | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E117_E11.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E117_E11.py.snap index fa8f00ee24f4b..c3b25d40f40f6 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E117_E11.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E117_E11.py.snap @@ -25,8 +25,8 @@ E11.py:42:1: E117 Over-indented | 42 | #: E117 W191 43 | def start(): -44 | print() - | E117 +44 | print() + | ^^^^^^^^ E117 | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap index 65ff9adf5c091..198479926f3aa 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap @@ -27,34 +27,34 @@ E20.py:6:15: E201 Whitespace after '(' 8 | spam(ham[1], { eggs: 2}) | E201 9 | #: E201:1:6 -10 | spam( ham[1], {eggs: 2}) +10 | spam( ham[1], {eggs: 2}) | E20.py:8:6: E201 Whitespace after '(' | 8 | spam(ham[1], { eggs: 2}) 9 | #: E201:1:6 -10 | spam( ham[1], {eggs: 2}) +10 | spam( ham[1], {eggs: 2}) | E201 11 | #: E201:1:10 -12 | spam(ham[ 1], {eggs: 2}) +12 | spam(ham[ 1], {eggs: 2}) | E20.py:10:10: E201 Whitespace after '(' | -10 | spam( ham[1], {eggs: 2}) +10 | spam( ham[1], {eggs: 2}) 11 | #: E201:1:10 -12 | spam(ham[ 1], {eggs: 2}) +12 | spam(ham[ 1], {eggs: 2}) | E201 13 | #: E201:1:15 -14 | spam(ham[1], { eggs: 2}) +14 | spam(ham[1], { eggs: 2}) | E20.py:12:15: E201 Whitespace after '(' | -12 | spam(ham[ 1], {eggs: 2}) +12 | spam(ham[ 1], {eggs: 2}) 13 | #: E201:1:15 -14 | spam(ham[1], { eggs: 2}) +14 | spam(ham[1], { eggs: 2}) | E201 15 | #: Okay 16 | spam(ham[1], {eggs: 2}) diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap index 022fe45994048..1215dbb53bb98 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap @@ -27,34 +27,34 @@ E20.py:23:11: E202 Whitespace before ')' 25 | spam(ham[1 ], {eggs: 2}) | E202 26 | #: E202:1:23 -27 | spam(ham[1], {eggs: 2} ) +27 | spam(ham[1], {eggs: 2} ) | E20.py:25:23: E202 Whitespace before ')' | 25 | spam(ham[1 ], {eggs: 2}) 26 | #: E202:1:23 -27 | spam(ham[1], {eggs: 2} ) +27 | spam(ham[1], {eggs: 2} ) | E202 28 | #: E202:1:22 -29 | spam(ham[1], {eggs: 2 }) +29 | spam(ham[1], {eggs: 2 }) | E20.py:27:22: E202 Whitespace before ')' | -27 | spam(ham[1], {eggs: 2} ) +27 | spam(ham[1], {eggs: 2} ) 28 | #: E202:1:22 -29 | spam(ham[1], {eggs: 2 }) +29 | spam(ham[1], {eggs: 2 }) | E202 30 | #: E202:1:11 -31 | spam(ham[1 ], {eggs: 2}) +31 | spam(ham[1 ], {eggs: 2}) | E20.py:29:11: E202 Whitespace before ')' | -29 | spam(ham[1], {eggs: 2 }) +29 | spam(ham[1], {eggs: 2 }) 30 | #: E202:1:11 -31 | spam(ham[1 ], {eggs: 2}) +31 | spam(ham[1 ], {eggs: 2}) | E202 32 | #: Okay 33 | spam(ham[1], {eggs: 2}) diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap index 4f2d7c1c22a63..b59e240bd7f46 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap @@ -14,7 +14,7 @@ E20.py:55:10: E203 Whitespace before ',', ';', or ':' | 55 | x, y = y, x 56 | #: E203:1:10 -57 | if x == 4 : +57 | if x == 4 : | E203 58 | print x, y 59 | x, y = y, x @@ -34,7 +34,7 @@ E20.py:63:15: E203 Whitespace before ',', ';', or ':' | 63 | #: E203:2:15 E702:2:16 64 | if x == 4: -65 | print x, y ; x, y = y, x +65 | print x, y ; x, y = y, x | E203 66 | #: E203:3:13 67 | if x == 4: @@ -54,7 +54,7 @@ E20.py:71:13: E203 Whitespace before ',', ';', or ':' | 71 | if x == 4: 72 | print x, y -73 | x, y = y , x +73 | x, y = y , x | E203 74 | #: Okay 75 | if x == 4: diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap index fad6ed3133671..0e7405603f814 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap @@ -5,7 +5,7 @@ E22.py:43:2: E223 Tab before operator | 43 | #: E223 44 | foobart = 4 -45 | a = 3 # aligned with tab +45 | a = 3 # aligned with tab | E223 46 | #: | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap index cdb3d18aa44ca..a8344802ea318 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap @@ -4,7 +4,7 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs E22.py:48:5: E224 Tab after operator | 48 | #: E224 -49 | a += 1 +49 | a += 1 | E224 50 | b += 1000 51 | #: diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap index b05c7d68b0bae..af6e1e52f0b06 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap @@ -28,12 +28,12 @@ E27.py:8:3: E271 Multiple spaces after keyword 10 | if 1: | E271 11 | #: E273 -12 | True and False +12 | True and False | E27.py:14:6: E271 Multiple spaces after keyword | -14 | True and False +14 | True and False 15 | #: E271 16 | a and b | E271 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap index fa61e8f61fa3f..1bc20e60e5be3 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap @@ -28,7 +28,7 @@ E27.py:24:5: E272 Multiple spaces before keyword 26 | this and False | E272 27 | #: E273 -28 | a and b +28 | a and b | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap index 9fefdace978da..87dce5836355a 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap @@ -5,17 +5,17 @@ E27.py:10:9: E273 Tab after keyword | 10 | if 1: 11 | #: E273 -12 | True and False +12 | True and False | E273 13 | #: E273 E274 -14 | True and False +14 | True and False | E27.py:12:5: E273 Tab after keyword | -12 | True and False +12 | True and False 13 | #: E273 E274 -14 | True and False +14 | True and False | E273 15 | #: E271 16 | a and b @@ -23,10 +23,10 @@ E27.py:12:5: E273 Tab after keyword E27.py:12:10: E273 Tab after keyword | -12 | True and False +12 | True and False 13 | #: E273 E274 -14 | True and False - | E273 +14 | True and False + | E273 15 | #: E271 16 | a and b | @@ -35,18 +35,18 @@ E27.py:26:6: E273 Tab after keyword | 26 | this and False 27 | #: E273 -28 | a and b +28 | a and b | E273 29 | #: E274 -30 | a and b +30 | a and b | E27.py:30:10: E273 Tab after keyword | -30 | a and b +30 | a and b 31 | #: E273 E274 -32 | this and False - | E273 +32 | this and False + | E273 33 | #: Okay 34 | from u import (a, b) | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap index 9adc314ea5130..1150a45c696c7 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap @@ -3,20 +3,20 @@ source: crates/ruff/src/rules/pycodestyle/mod.rs --- E27.py:28:3: E274 Tab before keyword | -28 | a and b +28 | a and b 29 | #: E274 -30 | a and b - | E274 +30 | a and b + | E274 31 | #: E273 E274 -32 | this and False +32 | this and False | E27.py:30:6: E274 Tab before keyword | -30 | a and b +30 | a and b 31 | #: E273 E274 -32 | this and False - | E274 +32 | this and False + | E274 33 | #: Okay 34 | from u import (a, b) | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap index 1dd9828f0500d..949a97f1bced4 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W191_W19.py.snap @@ -5,8 +5,8 @@ W19.py:3:1: W191 Indentation contains tabs | 3 | #: W191 4 | if False: -5 | print # indented with 1 tab - | W191 +5 | print # indented with 1 tab + | ^^^^ W191 6 | #: | @@ -14,8 +14,8 @@ W19.py:9:1: W191 Indentation contains tabs | 9 | #: W191 10 | y = x == 2 \ -11 | or x == 3 - | W191 +11 | or x == 3 + | ^^^^ W191 12 | #: E101 W191 W504 13 | if ( | @@ -24,8 +24,8 @@ W19.py:16:1: W191 Indentation contains tabs | 16 | ) or 17 | y == 4): -18 | pass - | W191 +18 | pass + | ^^^^ W191 19 | #: E101 W191 20 | if x == 2 \ | @@ -34,8 +34,8 @@ W19.py:21:1: W191 Indentation contains tabs | 21 | or y > 1 \ 22 | or x == 3: -23 | pass - | W191 +23 | pass + | ^^^^ W191 24 | #: E101 W191 25 | if x == 2 \ | @@ -44,8 +44,8 @@ W19.py:26:1: W191 Indentation contains tabs | 26 | or y > 1 \ 27 | or x == 3: -28 | pass - | W191 +28 | pass + | ^^^^ W191 29 | #: | @@ -53,8 +53,8 @@ W19.py:32:1: W191 Indentation contains tabs | 32 | if (foo == bar and 33 | baz == bop): -34 | pass - | W191 +34 | pass + | ^^^^ W191 35 | #: E101 W191 W504 36 | if ( | @@ -63,8 +63,8 @@ W19.py:38:1: W191 Indentation contains tabs | 38 | baz == bop 39 | ): -40 | pass - | W191 +40 | pass + | ^^^^ W191 41 | #: | @@ -72,18 +72,18 @@ W19.py:44:1: W191 Indentation contains tabs | 44 | if start[1] > end_col and not ( 45 | over_indent == 4 and indent_next): -46 | return (0, "E121 continuation line over-" - | W191 -47 | "indented for visual indent") +46 | return (0, "E121 continuation line over-" + | ^^^^ W191 +47 | "indented for visual indent") 48 | #: | W19.py:45:1: W191 Indentation contains tabs | 45 | over_indent == 4 and indent_next): -46 | return (0, "E121 continuation line over-" -47 | "indented for visual indent") - | ^^^^^^^^ W191 +46 | return (0, "E121 continuation line over-" +47 | "indented for visual indent") + | ^^^^^^^^^^^^ W191 48 | #: | @@ -91,8 +91,8 @@ W19.py:54:1: W191 Indentation contains tabs | 54 | var_one, var_two, var_three, 55 | var_four): -56 | print(var_one) - | W191 +56 | print(var_one) + | ^^^^ W191 57 | #: E101 W191 W504 58 | if ((row < 0 or self.moduleCount <= row or | @@ -101,8 +101,8 @@ W19.py:58:1: W191 Indentation contains tabs | 58 | if ((row < 0 or self.moduleCount <= row or 59 | col < 0 or self.moduleCount <= col)): -60 | raise Exception("%s,%s - %s" % (row, col, self.moduleCount)) - | W191 +60 | raise Exception("%s,%s - %s" % (row, col, self.moduleCount)) + | ^^^^ W191 61 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191 62 | if bar: | @@ -111,58 +111,58 @@ W19.py:61:1: W191 Indentation contains tabs | 61 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191 62 | if bar: -63 | return ( - | W191 -64 | start, 'E121 lines starting with a ' -65 | 'closing bracket should be indented ' +63 | return ( + | ^^^^ W191 +64 | start, 'E121 lines starting with a ' +65 | 'closing bracket should be indented ' | W19.py:62:1: W191 Indentation contains tabs | 62 | if bar: -63 | return ( -64 | start, 'E121 lines starting with a ' - | ^^^^ W191 -65 | 'closing bracket should be indented ' -66 | "to match that of the opening " +63 | return ( +64 | start, 'E121 lines starting with a ' + | ^^^^^^^^ W191 +65 | 'closing bracket should be indented ' +66 | "to match that of the opening " | W19.py:63:1: W191 Indentation contains tabs | -63 | return ( -64 | start, 'E121 lines starting with a ' -65 | 'closing bracket should be indented ' - | ^^^^ W191 -66 | "to match that of the opening " -67 | "bracket's line" +63 | return ( +64 | start, 'E121 lines starting with a ' +65 | 'closing bracket should be indented ' + | ^^^^^^^^ W191 +66 | "to match that of the opening " +67 | "bracket's line" | W19.py:64:1: W191 Indentation contains tabs | -64 | start, 'E121 lines starting with a ' -65 | 'closing bracket should be indented ' -66 | "to match that of the opening " - | ^^^^ W191 -67 | "bracket's line" -68 | ) +64 | start, 'E121 lines starting with a ' +65 | 'closing bracket should be indented ' +66 | "to match that of the opening " + | ^^^^^^^^ W191 +67 | "bracket's line" +68 | ) | W19.py:65:1: W191 Indentation contains tabs | -65 | 'closing bracket should be indented ' -66 | "to match that of the opening " -67 | "bracket's line" - | ^^^^ W191 -68 | ) +65 | 'closing bracket should be indented ' +66 | "to match that of the opening " +67 | "bracket's line" + | ^^^^^^^^ W191 +68 | ) 69 | # | W19.py:66:1: W191 Indentation contains tabs | -66 | "to match that of the opening " -67 | "bracket's line" -68 | ) - | W191 +66 | "to match that of the opening " +67 | "bracket's line" +68 | ) + | ^^^^ W191 69 | # 70 | #: E101 W191 W504 | @@ -171,8 +171,8 @@ W19.py:73:1: W191 Indentation contains tabs | 73 | foo.bar("bop") 74 | )): -75 | print "yes" - | W191 +75 | print "yes" + | ^^^^ W191 76 | #: E101 W191 W504 77 | # also ok, but starting to look like LISP | @@ -181,8 +181,8 @@ W19.py:78:1: W191 Indentation contains tabs | 78 | if ((foo.bar("baz") and 79 | foo.bar("bop"))): -80 | print "yes" - | W191 +80 | print "yes" + | ^^^^ W191 81 | #: E101 W191 W504 82 | if (a == 2 or | @@ -191,8 +191,8 @@ W19.py:83:1: W191 Indentation contains tabs | 83 | b == "abc def ghi" 84 | "jkl mno"): -85 | return True - | W191 +85 | return True + | ^^^^ W191 86 | #: E101 W191 W504 87 | if (a == 2 or | @@ -201,8 +201,8 @@ W19.py:88:1: W191 Indentation contains tabs | 88 | b == """abc def ghi 89 | jkl mno"""): -90 | return True - | W191 +90 | return True + | ^^^^ W191 91 | #: W191:2:1 W191:3:1 E101:3:2 92 | if length > options.max_line_length: | @@ -211,35 +211,35 @@ W19.py:91:1: W191 Indentation contains tabs | 91 | #: W191:2:1 W191:3:1 E101:3:2 92 | if length > options.max_line_length: -93 | return options.max_line_length, \ - | W191 -94 | "E501 line too long (%d characters)" % length +93 | return options.max_line_length, \ + | ^^^^ W191 +94 | "E501 line too long (%d characters)" % length | W19.py:92:1: W191 Indentation contains tabs | 92 | if length > options.max_line_length: -93 | return options.max_line_length, \ -94 | "E501 line too long (%d characters)" % length - | ^^^^ W191 +93 | return options.max_line_length, \ +94 | "E501 line too long (%d characters)" % length + | ^^^^^^^^ W191 | W19.py:98:1: W191 Indentation contains tabs | 98 | #: E101 W191 W191 W504 99 | if os.path.exists(os.path.join(path, PEP8_BIN)): -100 | cmd = ([os.path.join(path, PEP8_BIN)] + - | W191 -101 | self._pep8_options(targetfile)) +100 | cmd = ([os.path.join(path, PEP8_BIN)] + + | ^^^^ W191 +101 | self._pep8_options(targetfile)) 102 | #: W191 - okay | W19.py:99:1: W191 Indentation contains tabs | 99 | if os.path.exists(os.path.join(path, PEP8_BIN)): -100 | cmd = ([os.path.join(path, PEP8_BIN)] + -101 | self._pep8_options(targetfile)) - | ^^^^^^^ W191 +100 | cmd = ([os.path.join(path, PEP8_BIN)] + +101 | self._pep8_options(targetfile)) + | ^^^^^^^^^^^ W191 102 | #: W191 - okay 103 | ''' | @@ -248,36 +248,36 @@ W19.py:125:1: W191 Indentation contains tabs | 125 | if foo is None and bar is "bop" and \ 126 | blah == 'yeah': -127 | blah = 'yeahnah' - | W191 +127 | blah = 'yeahnah' + | ^^^^ W191 | W19.py:131:1: W191 Indentation contains tabs | 131 | #: W191 W191 W191 132 | if True: -133 | foo( - | W191 -134 | 1, -135 | 2) +133 | foo( + | ^^^^ W191 +134 | 1, +135 | 2) | W19.py:132:1: W191 Indentation contains tabs | 132 | if True: -133 | foo( -134 | 1, - | W191 -135 | 2) +133 | foo( +134 | 1, + | ^^^^^^^^ W191 +135 | 2) 136 | #: W191 W191 W191 W191 W191 | W19.py:133:1: W191 Indentation contains tabs | -133 | foo( -134 | 1, -135 | 2) - | W191 +133 | foo( +134 | 1, +135 | 2) + | ^^^^^^^^ W191 136 | #: W191 W191 W191 W191 W191 137 | def test_keys(self): | @@ -286,48 +286,48 @@ W19.py:136:1: W191 Indentation contains tabs | 136 | #: W191 W191 W191 W191 W191 137 | def test_keys(self): -138 | """areas.json - All regions are accounted for.""" - | W191 -139 | expected = set([ -140 | u'Norrbotten', +138 | """areas.json - All regions are accounted for.""" + | ^^^^ W191 +139 | expected = set([ +140 | u'Norrbotten', | W19.py:137:1: W191 Indentation contains tabs | 137 | def test_keys(self): -138 | """areas.json - All regions are accounted for.""" -139 | expected = set([ - | W191 -140 | u'Norrbotten', -141 | u'V\xe4sterbotten', +138 | """areas.json - All regions are accounted for.""" +139 | expected = set([ + | ^^^^ W191 +140 | u'Norrbotten', +141 | u'V\xe4sterbotten', | W19.py:138:1: W191 Indentation contains tabs | -138 | """areas.json - All regions are accounted for.""" -139 | expected = set([ -140 | u'Norrbotten', - | W191 -141 | u'V\xe4sterbotten', -142 | ]) +138 | """areas.json - All regions are accounted for.""" +139 | expected = set([ +140 | u'Norrbotten', + | ^^^^^^^^ W191 +141 | u'V\xe4sterbotten', +142 | ]) | W19.py:139:1: W191 Indentation contains tabs | -139 | expected = set([ -140 | u'Norrbotten', -141 | u'V\xe4sterbotten', - | W191 -142 | ]) +139 | expected = set([ +140 | u'Norrbotten', +141 | u'V\xe4sterbotten', + | ^^^^^^^^ W191 +142 | ]) 143 | #: W191 | W19.py:140:1: W191 Indentation contains tabs | -140 | u'Norrbotten', -141 | u'V\xe4sterbotten', -142 | ]) - | W191 +140 | u'Norrbotten', +141 | u'V\xe4sterbotten', +142 | ]) + | ^^^^ W191 143 | #: W191 144 | x = [ | @@ -336,8 +336,8 @@ W19.py:143:1: W191 Indentation contains tabs | 143 | #: W191 144 | x = [ -145 | 'abc' - | W191 +145 | 'abc' + | ^^^^ W191 146 | ] 147 | #: W191 - okay | From ccfc78e2d5d886ec26f3c740189a320eee59dc15 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 3 May 2023 01:19:38 +0200 Subject: [PATCH 25/32] faq: Clarify how Ruff and Black treat line-length. (#4180) --- docs/faq.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index af7693b0492ec..4cb6e3c8fa8e6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -8,6 +8,12 @@ the `line-length` setting is consistent between the two. As a project, Ruff is designed to be used alongside Black and, as such, will defer implementing stylistic lint rules that are obviated by autoformatting. +Note that Ruff and Black treat line-length enforcement a little differently. Black makes a +best-effort attempt to adhere to the `line-length`, but avoids automatic line-wrapping in some cases +(e.g., within comments). Ruff, on the other hand, will flag rule `E501` for any line that exceeds +the `line-length` setting. As such, if `E501` is enabled, Ruff can still trigger line-length +violations even when Black is enabled. + ## How does Ruff compare to Flake8? (Coming from Flake8? Try [`flake8-to-ruff`](https://pypi.org/project/flake8-to-ruff/) to From d0e3ca29d913a91bec33b3e90e43ccb12d557c42 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 3 May 2023 14:50:03 +0300 Subject: [PATCH 26/32] Print out autofix-broken or non-converging code when debugging (#4201) --- crates/ruff/src/linter.rs | 98 ++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index 8271bb74ea063..54895500b8477 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -453,24 +453,7 @@ pub fn lint_fix<'a>( // longer parseable on a subsequent pass, then we've introduced a // syntax error. Return the original code. if parseable && result.error.is_some() { - #[allow(clippy::print_stderr)] - { - eprintln!( - r#" -{}: Autofix introduced a syntax error. Reverting all changes. - -This indicates a bug in `{}`. If you could open an issue at: - - {}/issues/new?title=%5BAutofix%20error%5D - -...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative! -"#, - "error".red().bold(), - CARGO_PKG_NAME, - CARGO_PKG_REPOSITORY, - fs::relativize_path(path), - ); - } + report_autofix_syntax_error(path, &transformed, &result.error.unwrap()); return Err(anyhow!("Autofix introduced a syntax error")); } } @@ -493,25 +476,7 @@ This indicates a bug in `{}`. If you could open an issue at: continue; } - #[allow(clippy::print_stderr)] - { - eprintln!( - r#" -{}: Failed to converge after {} iterations. - -This indicates a bug in `{}`. If you could open an issue at: - - {}/issues/new?title=%5BInfinite%20loop%5D - -...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative! -"#, - "error".red().bold(), - MAX_ITERATIONS, - CARGO_PKG_NAME, - CARGO_PKG_REPOSITORY, - fs::relativize_path(path), - ); - } + report_failed_to_converge_error(path, &transformed); } return Ok(FixerResult { @@ -526,3 +491,62 @@ This indicates a bug in `{}`. If you could open an issue at: }); } } + +#[allow(clippy::print_stderr)] +fn report_failed_to_converge_error(path: &Path, transformed: &str) { + if cfg!(debug_assertions) { + eprintln!( + "{}: Failed to converge after {} iterations in `{}`:---\n{}\n---", + "debug error".red().bold(), + MAX_ITERATIONS, + fs::relativize_path(path), + transformed, + ); + } else { + eprintln!( + r#" +{}: Failed to converge after {} iterations. + +This indicates a bug in `{}`. If you could open an issue at: + + {}/issues/new?title=%5BInfinite%20loop%5D + +...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative! +"#, + "error".red().bold(), + MAX_ITERATIONS, + CARGO_PKG_NAME, + CARGO_PKG_REPOSITORY, + fs::relativize_path(path), + ); + } +} + +#[allow(clippy::print_stderr)] +fn report_autofix_syntax_error(path: &Path, transformed: &str, error: &ParseError) { + if cfg!(debug_assertions) { + eprintln!( + "{}: Autofix introduced a syntax error in `{}`: {}\n---\n{}\n---", + "debug error".red().bold(), + fs::relativize_path(path), + error, + transformed, + ); + } else { + eprintln!( + r#" +{}: Autofix introduced a syntax error. Reverting all changes. + +This indicates a bug in `{}`. If you could open an issue at: + + {}/issues/new?title=%5BAutofix%20error%5D + +...quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd be very appreciative! +"#, + "error".red().bold(), + CARGO_PKG_NAME, + CARGO_PKG_REPOSITORY, + fs::relativize_path(path), + ); + } +} From 460023a959c6881942fb8fb9c95708fb8fbc746a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leiser=20Fern=C3=A1ndez=20Gallo?= Date: Wed, 3 May 2023 15:48:43 +0200 Subject: [PATCH 27/32] Fix era panic caused by out of bound edition (#4206) --- .../resources/test/fixtures/eradicate/ERA001.py | 5 +++++ crates/ruff/src/rules/eradicate/rules.rs | 7 ++----- ...rules__eradicate__tests__ERA001_ERA001.py.snap | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/eradicate/ERA001.py b/crates/ruff/resources/test/fixtures/eradicate/ERA001.py index b12529624a993..a21d51c4ca94d 100644 --- a/crates/ruff/resources/test/fixtures/eradicate/ERA001.py +++ b/crates/ruff/resources/test/fixtures/eradicate/ERA001.py @@ -14,3 +14,8 @@ def foo(x, y, z): return False #import os # noqa: ERA001 + + +class A(): + pass + # b = c diff --git a/crates/ruff/src/rules/eradicate/rules.rs b/crates/ruff/src/rules/eradicate/rules.rs index ff0380165b1ce..2a67f4ec9ea5e 100644 --- a/crates/ruff/src/rules/eradicate/rules.rs +++ b/crates/ruff/src/rules/eradicate/rules.rs @@ -1,4 +1,4 @@ -use ruff_text_size::{TextLen, TextRange}; +use ruff_text_size::TextRange; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; @@ -58,10 +58,7 @@ pub fn commented_out_code( if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) { let mut diagnostic = Diagnostic::new(CommentedOutCode, range); if autofix.into() && settings.rules.should_fix(Rule::CommentedOutCode) { - diagnostic.set_fix(Edit::range_deletion(TextRange::at( - range.start(), - line.text_len(), - ))); + diagnostic.set_fix(Edit::range_deletion(locator.full_lines_range(range))); } Some(diagnostic) } else { diff --git a/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap b/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap index 4005ebdb4a6bf..4c60ac1639cd1 100644 --- a/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap +++ b/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap @@ -91,4 +91,19 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code 15 14 | 16 15 | #import os # noqa: ERA001 +ERA001.py:21:5: ERA001 [*] Found commented-out code + | +21 | class A(): +22 | pass +23 | # b = c + | ^^^^^^^ ERA001 + | + = help: Remove commented-out code + +ℹ Suggested fix +18 18 | +19 19 | class A(): +20 20 | pass +21 |- # b = c + From 37aae666c717972534442ca6cf8095138994461a Mon Sep 17 00:00:00 2001 From: Arya Kumar Date: Wed, 3 May 2023 22:37:32 -0400 Subject: [PATCH 28/32] [flake8-pyi] PYI020 (#4211) --- .../test/fixtures/flake8_pyi/PYI020.py | 28 +++ .../test/fixtures/flake8_pyi/PYI020.pyi | 28 +++ crates/ruff/src/checkers/ast/mod.rs | 5 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/registry.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 + crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 + .../rules/quoted_annotation_in_stub.rs | 29 +++ ...__flake8_pyi__tests__PYI020_PYI020.py.snap | 4 + ..._flake8_pyi__tests__PYI020_PYI020.pyi.snap | 187 ++++++++++++++++++ ruff.schema.json | 1 + 11 files changed, 288 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py new file mode 100644 index 0000000000000..33af00bef792b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py @@ -0,0 +1,28 @@ +import sys +import typing +from typing import Annotated, Literal, TypeAlias, TypeVar + +import typing_extensions + +def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs +def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs +_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs + +def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... + +def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs +Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs + +class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs + """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs + +if sys.platform == "linux": + f: "int" # Y020 Quoted annotations should never be used in stubs +elif sys.platform == "win32": + f: "str" # Y020 Quoted annotations should never be used in stubs +else: + f: "bytes" # Y020 Quoted annotations should never be used in stubs + +# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations" +k = "" # Y052 Need type annotation for "k" +el = r"" # Y052 Need type annotation for "el" diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi new file mode 100644 index 0000000000000..33af00bef792b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi @@ -0,0 +1,28 @@ +import sys +import typing +from typing import Annotated, Literal, TypeAlias, TypeVar + +import typing_extensions + +def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs +def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs +_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs + +def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... + +def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs +Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs + +class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs + """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs + +if sys.platform == "linux": + f: "int" # Y020 Quoted annotations should never be used in stubs +elif sys.platform == "win32": + f: "str" # Y020 Quoted annotations should never be used in stubs +else: + f: "bytes" # Y020 Quoted annotations should never be used in stubs + +# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations" +k = "" # Y052 Need type annotation for "k" +el = r"" # Y052 Need type annotation for "el" diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index a41407746651c..486784b17a390 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4790,6 +4790,11 @@ impl<'a> Checker<'a> { pyupgrade::rules::quoted_annotation(self, value, range); } } + if self.is_stub { + if self.settings.rules.enabled(Rule::QuotedAnnotationInStub) { + flake8_pyi::rules::quoted_annotation_in_stub(self, value, range); + } + } let expr = allocator.alloc(expr); diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index bbb33c41d45cd..84d894ac178bc 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -592,6 +592,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Pyi, "014") => Rule::ArgumentDefaultInStub, (Flake8Pyi, "015") => Rule::AssignmentDefaultInStub, (Flake8Pyi, "016") => Rule::DuplicateUnionMember, + (Flake8Pyi, "020") => Rule::QuotedAnnotationInStub, (Flake8Pyi, "021") => Rule::DocstringInStub, (Flake8Pyi, "033") => Rule::TypeCommentInStub, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 22eabf0636658..98eb0f0d71d7d 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -539,6 +539,7 @@ ruff_macros::register_rules!( rules::flake8_pyi::rules::UnrecognizedPlatformName, rules::flake8_pyi::rules::PassInClassBody, rules::flake8_pyi::rules::DuplicateUnionMember, + rules::flake8_pyi::rules::QuotedAnnotationInStub, // flake8-pytest-style rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle, rules::flake8_pytest_style::rules::PytestFixturePositionalArgs, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 2165dace1065f..ac0878f3fda6c 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -35,6 +35,8 @@ mod tests { #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))] #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))] #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))] + #[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.py"))] + #[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index b97c1ce2a7601..62c72b628b731 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -5,6 +5,7 @@ pub use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; pub use pass_in_class_body::{pass_in_class_body, PassInClassBody}; pub use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody}; pub use prefix_type_params::{prefix_type_params, UnprefixedTypeParam}; +pub use quoted_annotation_in_stub::{quoted_annotation_in_stub, QuotedAnnotationInStub}; pub use simple_defaults::{ annotated_assignment_default_in_stub, argument_simple_defaults, assignment_default_in_stub, typed_argument_simple_defaults, ArgumentDefaultInStub, AssignmentDefaultInStub, @@ -22,6 +23,7 @@ mod non_empty_stub_body; mod pass_in_class_body; mod pass_statement_stub_body; mod prefix_type_params; +mod quoted_annotation_in_stub; mod simple_defaults; mod type_comment_in_stub; mod unrecognized_platform; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs new file mode 100644 index 0000000000000..adde48e27a0ce --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs @@ -0,0 +1,29 @@ +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_text_size::TextRange; + +use crate::checkers::ast::Checker; +use crate::registry::Rule; + +#[violation] +pub struct QuotedAnnotationInStub; + +impl AlwaysAutofixableViolation for QuotedAnnotationInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!("Quoted annotations should not be included in stubs") + } + + fn autofix_title(&self) -> String { + "Remove quotes".to_string() + } +} + +/// PYI020 +pub fn quoted_annotation_in_stub(checker: &mut Checker, annotation: &str, range: TextRange) { + let mut diagnostic = Diagnostic::new(QuotedAnnotationInStub, range); + if checker.patch(Rule::QuotedAnnotationInStub) { + diagnostic.set_fix(Edit::range_replacement(annotation.to_string(), range)); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.py.snap new file mode 100644 index 0000000000000..d1aa2e9116558 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap new file mode 100644 index 0000000000000..ce3777c3b7320 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap @@ -0,0 +1,187 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI020.pyi:7:10: PYI020 [*] Quoted annotations should not be included in stubs + | + 7 | import typing_extensions + 8 | + 9 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +10 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs +11 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs + | + = help: Remove quotes + +ℹ Suggested fix +4 4 | +5 5 | import typing_extensions +6 6 | +7 |-def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs + 7 |+def f(x: int): ... # Y020 Quoted annotations should never be used in stubs +8 8 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs +9 9 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs +10 10 | + +PYI020.pyi:8:15: PYI020 [*] Quoted annotations should not be included in stubs + | + 8 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs + 9 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +10 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs + | + = help: Remove quotes + +ℹ Suggested fix +5 5 | import typing_extensions +6 6 | +7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs +8 |-def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs + 8 |+def g(x: list[int]): ... # Y020 Quoted annotations should never be used in stubs +9 9 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs +10 10 | +11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... + +PYI020.pyi:9:26: PYI020 [*] Quoted annotations should not be included in stubs + | + 9 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs +10 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs +11 | _T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +12 | +13 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... + | + = help: Remove quotes + +ℹ Suggested fix +6 6 | +7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs +8 8 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs +9 |-_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs + 9 |+_T = TypeVar("_T", bound=int) # Y020 Quoted annotations should never be used in stubs +10 10 | +11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... +12 12 | + +PYI020.pyi:13:12: PYI020 [*] Quoted annotations should not be included in stubs + | +13 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... +14 | +15 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +16 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs + | + = help: Remove quotes + +ℹ Suggested fix +10 10 | +11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... +12 12 | +13 |-def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs + 13 |+def j() -> int: ... # Y020 Quoted annotations should never be used in stubs +14 14 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs +15 15 | +16 16 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs + +PYI020.pyi:14:25: PYI020 [*] Quoted annotations should not be included in stubs + | +14 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs +15 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +16 | +17 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs + | + = help: Remove quotes + +ℹ Suggested fix +11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... +12 12 | +13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs +14 |-Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs + 14 |+Alias: TypeAlias = list[int] # Y020 Quoted annotations should never be used in stubs +15 15 | +16 16 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs +17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs + +PYI020.pyi:16:18: PYI020 [*] Quoted annotations should not be included in stubs + | +16 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs +17 | +18 | class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +19 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs + | + = help: Remove quotes + +ℹ Suggested fix +13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs +14 14 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs +15 15 | +16 |-class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs + 16 |+class Child(list[int]): # Y020 Quoted annotations should never be used in stubs +17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs +18 18 | +19 19 | if sys.platform == "linux": + +PYI020.pyi:20:8: PYI020 [*] Quoted annotations should not be included in stubs + | +20 | if sys.platform == "linux": +21 | f: "int" # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +22 | elif sys.platform == "win32": +23 | f: "str" # Y020 Quoted annotations should never be used in stubs + | + = help: Remove quotes + +ℹ Suggested fix +17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs +18 18 | +19 19 | if sys.platform == "linux": +20 |- f: "int" # Y020 Quoted annotations should never be used in stubs + 20 |+ f: int # Y020 Quoted annotations should never be used in stubs +21 21 | elif sys.platform == "win32": +22 22 | f: "str" # Y020 Quoted annotations should never be used in stubs +23 23 | else: + +PYI020.pyi:22:8: PYI020 [*] Quoted annotations should not be included in stubs + | +22 | f: "int" # Y020 Quoted annotations should never be used in stubs +23 | elif sys.platform == "win32": +24 | f: "str" # Y020 Quoted annotations should never be used in stubs + | ^^^^^ PYI020 +25 | else: +26 | f: "bytes" # Y020 Quoted annotations should never be used in stubs + | + = help: Remove quotes + +ℹ Suggested fix +19 19 | if sys.platform == "linux": +20 20 | f: "int" # Y020 Quoted annotations should never be used in stubs +21 21 | elif sys.platform == "win32": +22 |- f: "str" # Y020 Quoted annotations should never be used in stubs + 22 |+ f: str # Y020 Quoted annotations should never be used in stubs +23 23 | else: +24 24 | f: "bytes" # Y020 Quoted annotations should never be used in stubs +25 25 | + +PYI020.pyi:24:8: PYI020 [*] Quoted annotations should not be included in stubs + | +24 | f: "str" # Y020 Quoted annotations should never be used in stubs +25 | else: +26 | f: "bytes" # Y020 Quoted annotations should never be used in stubs + | ^^^^^^^ PYI020 +27 | +28 | # These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations" + | + = help: Remove quotes + +ℹ Suggested fix +21 21 | elif sys.platform == "win32": +22 22 | f: "str" # Y020 Quoted annotations should never be used in stubs +23 23 | else: +24 |- f: "bytes" # Y020 Quoted annotations should never be used in stubs + 24 |+ f: bytes # Y020 Quoted annotations should never be used in stubs +25 25 | +26 26 | # These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations" +27 27 | k = "" # Y052 Need type annotation for "k" + + diff --git a/ruff.schema.json b/ruff.schema.json index 6e9f2d2ef9840..64d99d10f240f 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2124,6 +2124,7 @@ "PYI015", "PYI016", "PYI02", + "PYI020", "PYI021", "PYI03", "PYI033", From 59d40f9f81056a8433db4de490857a6a23f93d2c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 4 May 2023 11:52:31 +0530 Subject: [PATCH 29/32] Show settings path in `--show-settings` output (#4199) --- crates/ruff/src/packaging.rs | 8 +- crates/ruff/src/resolver.rs | 110 +++++++++++------- crates/ruff_cli/src/commands/add_noqa.rs | 10 +- crates/ruff_cli/src/commands/run.rs | 30 ++--- crates/ruff_cli/src/commands/run_stdin.rs | 22 ++-- crates/ruff_cli/src/commands/show_files.rs | 6 +- crates/ruff_cli/src/commands/show_settings.rs | 11 +- crates/ruff_cli/src/lib.rs | 22 ++-- crates/ruff_cli/src/resolve.rs | 35 ++++-- 9 files changed, 152 insertions(+), 102 deletions(-) diff --git a/crates/ruff/src/packaging.rs b/crates/ruff/src/packaging.rs index 59f0400b0a59e..c80eedecd0e2e 100644 --- a/crates/ruff/src/packaging.rs +++ b/crates/ruff/src/packaging.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use rustc_hash::FxHashMap; -use crate::resolver::{PyprojectDiscovery, Resolver}; +use crate::resolver::{PyprojectConfig, Resolver}; // If we have a Python package layout like: // - root/ @@ -82,7 +82,7 @@ fn detect_package_root_with_cache<'a>( pub fn detect_package_roots<'a>( files: &[&'a Path], resolver: &'a Resolver, - pyproject_strategy: &'a PyprojectDiscovery, + pyproject_config: &'a PyprojectConfig, ) -> FxHashMap<&'a Path, Option<&'a Path>> { // Pre-populate the module cache, since the list of files could (but isn't // required to) contain some `__init__.py` files. @@ -98,9 +98,7 @@ pub fn detect_package_roots<'a>( // Search for the package root for each file. let mut package_roots: FxHashMap<&Path, Option<&Path>> = FxHashMap::default(); for file in files { - let namespace_packages = &resolver - .resolve(file, pyproject_strategy) - .namespace_packages; + let namespace_packages = &resolver.resolve(file, pyproject_config).namespace_packages; if let Some(package) = file.parent() { if package_roots.contains_key(package) { continue; diff --git a/crates/ruff/src/resolver.rs b/crates/ruff/src/resolver.rs index 41ac96c7d142c..0ad0fa244ed86 100644 --- a/crates/ruff/src/resolver.rs +++ b/crates/ruff/src/resolver.rs @@ -17,25 +17,42 @@ use crate::settings::configuration::Configuration; use crate::settings::pyproject::settings_toml; use crate::settings::{pyproject, AllSettings, Settings}; +/// The configuration information from a `pyproject.toml` file. +pub struct PyprojectConfig { + /// The strategy used to discover the relevant `pyproject.toml` file for + /// each Python file. + pub strategy: PyprojectDiscoveryStrategy, + /// All settings from the `pyproject.toml` file. + pub settings: AllSettings, + /// Absolute path to the `pyproject.toml` file. This would be `None` when + /// either using the default settings or the `--isolated` flag is set. + pub path: Option, +} + +impl PyprojectConfig { + pub fn new( + strategy: PyprojectDiscoveryStrategy, + settings: AllSettings, + path: Option, + ) -> Self { + Self { + strategy, + settings, + path: path.map(fs::normalize_path), + } + } +} + /// The strategy used to discover the relevant `pyproject.toml` file for each /// Python file. #[derive(Debug, is_macro::Is)] -pub enum PyprojectDiscovery { +pub enum PyprojectDiscoveryStrategy { /// Use a fixed `pyproject.toml` file for all Python files (i.e., one /// provided on the command-line). - Fixed(AllSettings), + Fixed, /// Use the closest `pyproject.toml` file in the filesystem hierarchy, or /// the default settings. - Hierarchical(AllSettings), -} - -impl PyprojectDiscovery { - pub fn top_level_settings(&self) -> &AllSettings { - match self { - PyprojectDiscovery::Fixed(settings) => settings, - PyprojectDiscovery::Hierarchical(settings) => settings, - } - } + Hierarchical, } /// The strategy for resolving file paths in a `pyproject.toml`. @@ -75,21 +92,25 @@ impl Resolver { pub fn resolve_all<'a>( &'a self, path: &Path, - strategy: &'a PyprojectDiscovery, + pyproject_config: &'a PyprojectConfig, ) -> &'a AllSettings { - match strategy { - PyprojectDiscovery::Fixed(settings) => settings, - PyprojectDiscovery::Hierarchical(default) => self + match pyproject_config.strategy { + PyprojectDiscoveryStrategy::Fixed => &pyproject_config.settings, + PyprojectDiscoveryStrategy::Hierarchical => self .settings .iter() .rev() .find_map(|(root, settings)| path.starts_with(root).then_some(settings)) - .unwrap_or(default), + .unwrap_or(&pyproject_config.settings), } } - pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a PyprojectDiscovery) -> &'a Settings { - &self.resolve_all(path, strategy).lib + pub fn resolve<'a>( + &'a self, + path: &Path, + pyproject_config: &'a PyprojectConfig, + ) -> &'a Settings { + &self.resolve_all(path, pyproject_config).lib } /// Return an iterator over the resolved [`Settings`] in this [`Resolver`]. @@ -200,7 +221,7 @@ fn match_exclusion, R: AsRef>( /// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths. pub fn python_files_in_path( paths: &[PathBuf], - pyproject_strategy: &PyprojectDiscovery, + pyproject_config: &PyprojectConfig, processor: impl ConfigProcessor, ) -> Result<(Vec>, Resolver)> { // Normalize every path (e.g., convert from relative to absolute). @@ -209,7 +230,7 @@ pub fn python_files_in_path( // Search for `pyproject.toml` files in all parent directories. let mut resolver = Resolver::default(); let mut seen = FxHashSet::default(); - if pyproject_strategy.is_hierarchical() { + if pyproject_config.strategy.is_hierarchical() { for path in &paths { for ancestor in path.ancestors() { if seen.insert(ancestor) { @@ -224,8 +245,8 @@ pub fn python_files_in_path( } // Check if the paths themselves are excluded. - if pyproject_strategy.top_level_settings().lib.force_exclude { - paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_strategy)); + if pyproject_config.settings.lib.force_exclude { + paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_config)); if paths.is_empty() { return Ok((vec![], resolver)); } @@ -240,12 +261,7 @@ pub fn python_files_in_path( for path in &paths[1..] { builder.add(path); } - builder.standard_filters( - pyproject_strategy - .top_level_settings() - .lib - .respect_gitignore, - ); + builder.standard_filters(pyproject_config.settings.lib.respect_gitignore); builder.hidden(false); let walker = builder.build_parallel(); @@ -261,7 +277,7 @@ pub fn python_files_in_path( if entry.depth() > 0 { let path = entry.path(); let resolver = resolver.read().unwrap(); - let settings = resolver.resolve(path, pyproject_strategy); + let settings = resolver.resolve(path, pyproject_config); if let Some(file_name) = path.file_name() { if !settings.exclude.is_empty() && match_exclusion(path, file_name, &settings.exclude) @@ -283,7 +299,7 @@ pub fn python_files_in_path( // Search for the `pyproject.toml` file in this directory, before we visit any // of its contents. - if pyproject_strategy.is_hierarchical() { + if pyproject_config.strategy.is_hierarchical() { if let Ok(entry) = &result { if entry .file_type() @@ -321,7 +337,7 @@ pub fn python_files_in_path( // Otherwise, check if the file is included. let path = entry.path(); let resolver = resolver.read().unwrap(); - let settings = resolver.resolve(path, pyproject_strategy); + let settings = resolver.resolve(path, pyproject_config); if settings.include.is_match(path) { debug!("Included path via `include`: {:?}", path); true @@ -348,10 +364,10 @@ pub fn python_files_in_path( /// Return `true` if the Python file at [`Path`] is _not_ excluded. pub fn python_file_at_path( path: &Path, - pyproject_strategy: &PyprojectDiscovery, + pyproject_config: &PyprojectConfig, processor: impl ConfigProcessor, ) -> Result { - if !pyproject_strategy.top_level_settings().lib.force_exclude { + if !pyproject_config.settings.lib.force_exclude { return Ok(true); } @@ -360,7 +376,7 @@ pub fn python_file_at_path( // Search for `pyproject.toml` files in all parent directories. let mut resolver = Resolver::default(); - if pyproject_strategy.is_hierarchical() { + if pyproject_config.strategy.is_hierarchical() { for ancestor in path.ancestors() { if let Some(pyproject) = settings_toml(ancestor)? { let (root, settings) = @@ -371,14 +387,14 @@ pub fn python_file_at_path( } // Check exclusions. - Ok(!is_file_excluded(&path, &resolver, pyproject_strategy)) + Ok(!is_file_excluded(&path, &resolver, pyproject_config)) } /// Return `true` if the given top-level [`Path`] should be excluded. fn is_file_excluded( path: &Path, resolver: &Resolver, - pyproject_strategy: &PyprojectDiscovery, + pyproject_strategy: &PyprojectConfig, ) -> bool { // TODO(charlie): Respect gitignore. for path in path.ancestors() { @@ -419,7 +435,7 @@ mod tests { use crate::resolver::{ is_file_excluded, match_exclusion, resolve_settings_with_processor, NoOpProcessor, - PyprojectDiscovery, Relativity, Resolver, + PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver, }; use crate::settings::pyproject::find_settings_toml; use crate::settings::types::FilePattern; @@ -560,25 +576,29 @@ mod tests { fn rooted_exclusion() -> Result<()> { let package_root = test_resource_path("package"); let resolver = Resolver::default(); - let ppd = PyprojectDiscovery::Hierarchical(resolve_settings_with_processor( - &find_settings_toml(&package_root)?.unwrap(), - &Relativity::Parent, - &NoOpProcessor, - )?); + let pyproject_config = PyprojectConfig::new( + PyprojectDiscoveryStrategy::Hierarchical, + resolve_settings_with_processor( + &find_settings_toml(&package_root)?.unwrap(), + &Relativity::Parent, + &NoOpProcessor, + )?, + None, + ); // src/app.py should not be excluded even if it lives in a hierarchy that should // be excluded by virtue of the pyproject.toml having `resources/*` in // it. assert!(!is_file_excluded( &package_root.join("src/app.py"), &resolver, - &ppd, + &pyproject_config, )); // However, resources/ignored.py should be ignored, since that `resources` is // beneath the package root. assert!(is_file_excluded( &package_root.join("resources/ignored.py"), &resolver, - &ppd, + &pyproject_config, )); Ok(()) } diff --git a/crates/ruff_cli/src/commands/add_noqa.rs b/crates/ruff_cli/src/commands/add_noqa.rs index 40aab27fc0ed8..b73d676d6b7de 100644 --- a/crates/ruff_cli/src/commands/add_noqa.rs +++ b/crates/ruff_cli/src/commands/add_noqa.rs @@ -7,7 +7,7 @@ use log::{debug, error}; use rayon::prelude::*; use ruff::linter::add_noqa_to_path; -use ruff::resolver::PyprojectDiscovery; +use ruff::resolver::PyprojectConfig; use ruff::{packaging, resolver, warn_user_once}; use crate::args::Overrides; @@ -15,12 +15,12 @@ use crate::args::Overrides; /// Add `noqa` directives to a collection of files. pub fn add_noqa( files: &[PathBuf], - pyproject_strategy: &PyprojectDiscovery, + pyproject_config: &PyprojectConfig, overrides: &Overrides, ) -> Result { // Collect all the files to check. let start = Instant::now(); - let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?; + let (paths, resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); @@ -37,7 +37,7 @@ pub fn add_noqa( .map(ignore::DirEntry::path) .collect::>(), &resolver, - pyproject_strategy, + pyproject_config, ); let start = Instant::now(); @@ -50,7 +50,7 @@ pub fn add_noqa( .parent() .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); - let settings = resolver.resolve(path, pyproject_strategy); + let settings = resolver.resolve(path, pyproject_config); match add_noqa_to_path(path, package, settings) { Ok(count) => Some(count), Err(e) => { diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 64da002afc939..6eea564bc19a1 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -12,7 +12,7 @@ use ruff_text_size::{TextRange, TextSize}; use ruff::message::Message; use ruff::registry::Rule; -use ruff::resolver::PyprojectDiscovery; +use ruff::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; use ruff::settings::{flags, AllSettings}; use ruff::{fs, packaging, resolver, warn_user_once, IOError}; use ruff_diagnostics::Diagnostic; @@ -27,7 +27,7 @@ use crate::panic::catch_unwind; /// Run the linter over a collection of files. pub fn run( files: &[PathBuf], - pyproject_strategy: &PyprojectDiscovery, + pyproject_config: &PyprojectConfig, overrides: &Overrides, cache: flags::Cache, noqa: flags::Noqa, @@ -35,7 +35,7 @@ pub fn run( ) -> Result { // Collect all the Python files to check. let start = Instant::now(); - let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?; + let (paths, resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); @@ -52,12 +52,12 @@ pub fn run( } } - match &pyproject_strategy { - PyprojectDiscovery::Fixed(settings) => { - init_cache(&settings.cli.cache_dir); + match pyproject_config.strategy { + PyprojectDiscoveryStrategy::Fixed => { + init_cache(&pyproject_config.settings.cli.cache_dir); } - PyprojectDiscovery::Hierarchical(default) => { - for settings in std::iter::once(default).chain(resolver.iter()) { + PyprojectDiscoveryStrategy::Hierarchical => { + for settings in std::iter::once(&pyproject_config.settings).chain(resolver.iter()) { init_cache(&settings.cli.cache_dir); } } @@ -72,7 +72,7 @@ pub fn run( .map(ignore::DirEntry::path) .collect::>(), &resolver, - pyproject_strategy, + pyproject_config, ); let start = Instant::now(); @@ -86,7 +86,7 @@ pub fn run( .parent() .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); - let settings = resolver.resolve_all(path, pyproject_strategy); + let settings = resolver.resolve_all(path, pyproject_config); lint_path(path, package, settings, cache, noqa, autofix).map_err(|e| { (Some(path.to_owned()), { @@ -116,7 +116,7 @@ pub fn run( fs::relativize_path(path).bold(), ":".bold() ); - let settings = resolver.resolve(path, pyproject_strategy); + let settings = resolver.resolve(path, pyproject_config); if settings.rules.enabled(Rule::IOError) { let file = SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(); @@ -196,7 +196,7 @@ mod test { use path_absolutize::Absolutize; use ruff::logging::LogLevel; - use ruff::resolver::PyprojectDiscovery; + use ruff::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; use ruff::settings::configuration::{Configuration, RuleSelection}; use ruff::settings::flags::FixMode; use ruff::settings::flags::{Cache, Noqa}; @@ -238,7 +238,11 @@ mod test { let diagnostics = run( &[root_path.join("valid.ipynb")], - &PyprojectDiscovery::Fixed(AllSettings::from_configuration(configuration, &root_path)?), + &PyprojectConfig::new( + PyprojectDiscoveryStrategy::Fixed, + AllSettings::from_configuration(configuration, &root_path)?, + None, + ), &overrides, Cache::Disabled, Noqa::Enabled, diff --git a/crates/ruff_cli/src/commands/run_stdin.rs b/crates/ruff_cli/src/commands/run_stdin.rs index 3ca32e2f5ff33..b5d06d7da3fa2 100644 --- a/crates/ruff_cli/src/commands/run_stdin.rs +++ b/crates/ruff_cli/src/commands/run_stdin.rs @@ -3,7 +3,7 @@ use std::path::Path; use anyhow::Result; -use ruff::resolver::PyprojectDiscovery; +use ruff::resolver::PyprojectConfig; use ruff::settings::flags; use ruff::{packaging, resolver}; @@ -20,22 +20,28 @@ fn read_from_stdin() -> Result { /// Run the linter over a single file, read from `stdin`. pub fn run_stdin( filename: Option<&Path>, - pyproject_strategy: &PyprojectDiscovery, + pyproject_config: &PyprojectConfig, overrides: &Overrides, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { if let Some(filename) = filename { - if !resolver::python_file_at_path(filename, pyproject_strategy, overrides)? { + if !resolver::python_file_at_path(filename, pyproject_config, overrides)? { return Ok(Diagnostics::default()); } } - let settings = pyproject_strategy.top_level_settings(); - let package_root = filename - .and_then(Path::parent) - .and_then(|path| packaging::detect_package_root(path, &settings.lib.namespace_packages)); + let package_root = filename.and_then(Path::parent).and_then(|path| { + packaging::detect_package_root(path, &pyproject_config.settings.lib.namespace_packages) + }); let stdin = read_from_stdin()?; - let mut diagnostics = lint_stdin(filename, package_root, &stdin, &settings.lib, noqa, autofix)?; + let mut diagnostics = lint_stdin( + filename, + package_root, + &stdin, + &pyproject_config.settings.lib, + noqa, + autofix, + )?; diagnostics.messages.sort_unstable(); Ok(diagnostics) } diff --git a/crates/ruff_cli/src/commands/show_files.rs b/crates/ruff_cli/src/commands/show_files.rs index 93e4ca4946b2b..4efdae3ca99f0 100644 --- a/crates/ruff_cli/src/commands/show_files.rs +++ b/crates/ruff_cli/src/commands/show_files.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use anyhow::Result; use itertools::Itertools; -use ruff::resolver::PyprojectDiscovery; +use ruff::resolver::PyprojectConfig; use ruff::{resolver, warn_user_once}; use crate::args::Overrides; @@ -12,11 +12,11 @@ use crate::args::Overrides; /// Show the list of files to be checked based on current settings. pub fn show_files( files: &[PathBuf], - pyproject_strategy: &PyprojectDiscovery, + pyproject_config: &PyprojectConfig, overrides: &Overrides, ) -> Result<()> { // Collect all files in the hierarchy. - let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?; + let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); diff --git a/crates/ruff_cli/src/commands/show_settings.rs b/crates/ruff_cli/src/commands/show_settings.rs index 6d6cf34c46732..f0e152c3833a4 100644 --- a/crates/ruff_cli/src/commands/show_settings.rs +++ b/crates/ruff_cli/src/commands/show_settings.rs @@ -5,18 +5,18 @@ use anyhow::{bail, Result}; use itertools::Itertools; use ruff::resolver; -use ruff::resolver::PyprojectDiscovery; +use ruff::resolver::PyprojectConfig; use crate::args::Overrides; /// Print the user-facing configuration settings. pub fn show_settings( files: &[PathBuf], - pyproject_strategy: &PyprojectDiscovery, + pyproject_config: &PyprojectConfig, overrides: &Overrides, ) -> Result<()> { // Collect all files in the hierarchy. - let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?; + let (paths, resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; // Print the list of files. let Some(entry) = paths @@ -26,10 +26,13 @@ pub fn show_settings( bail!("No files found under the given path"); }; let path = entry.path(); - let settings = resolver.resolve(path, pyproject_strategy); + let settings = resolver.resolve(path, pyproject_config); let mut stdout = BufWriter::new(io::stdout().lock()); writeln!(stdout, "Resolved settings for: {path:?}")?; + if let Some(settings_path) = pyproject_config.path.as_ref() { + writeln!(stdout, "Settings path: {settings_path:?}")?; + } writeln!(stdout, "{settings:#?}")?; Ok(()) diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index a44ff3edb0092..2a0ef4ae42d88 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -97,7 +97,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { // Construct the "default" settings. These are used when no `pyproject.toml` // files are present, or files are injected from outside of the hierarchy. - let pyproject_strategy = resolve::resolve( + let pyproject_config = resolve::resolve( cli.isolated, cli.config.as_deref(), &overrides, @@ -105,16 +105,14 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { )?; if cli.show_settings { - commands::show_settings::show_settings(&cli.files, &pyproject_strategy, &overrides)?; + commands::show_settings::show_settings(&cli.files, &pyproject_config, &overrides)?; return Ok(ExitStatus::Success); } if cli.show_files { - commands::show_files::show_files(&cli.files, &pyproject_strategy, &overrides)?; + commands::show_files::show_files(&cli.files, &pyproject_config, &overrides)?; return Ok(ExitStatus::Success); } - let top_level_settings = pyproject_strategy.top_level_settings(); - // Extract options that are included in `Settings`, but only apply at the top // level. let CliSettings { @@ -124,7 +122,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { show_fixes, update_check, .. - } = top_level_settings.cli.clone(); + } = pyproject_config.settings.cli; // Autofix rules are as follows: // - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or @@ -155,7 +153,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { printer_flags |= PrinterFlags::SHOW_FIXES; } - if top_level_settings.lib.show_source { + if pyproject_config.settings.lib.show_source { printer_flags |= PrinterFlags::SHOW_SOURCE; } @@ -171,7 +169,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { warn_user_once!("--fix is incompatible with --add-noqa."); } let modifications = - commands::add_noqa::add_noqa(&cli.files, &pyproject_strategy, &overrides)?; + commands::add_noqa::add_noqa(&cli.files, &pyproject_config, &overrides)?; if modifications > 0 && log_level >= LogLevel::Default { let s = if modifications == 1 { "" } else { "s" }; #[allow(clippy::print_stderr)] @@ -195,7 +193,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { let messages = commands::run::run( &cli.files, - &pyproject_strategy, + &pyproject_config, &overrides, cache.into(), noqa.into(), @@ -225,7 +223,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { let messages = commands::run::run( &cli.files, - &pyproject_strategy, + &pyproject_config, &overrides, cache.into(), noqa.into(), @@ -244,7 +242,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { let diagnostics = if is_stdin { commands::run_stdin::run_stdin( cli.stdin_filename.map(fs::normalize_path).as_deref(), - &pyproject_strategy, + &pyproject_config, &overrides, noqa.into(), autofix, @@ -252,7 +250,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { } else { commands::run::run( &cli.files, - &pyproject_strategy, + &pyproject_config, &overrides, cache.into(), noqa.into(), diff --git a/crates/ruff_cli/src/resolve.rs b/crates/ruff_cli/src/resolve.rs index 40d3d809ca2ad..0bf5db66708f3 100644 --- a/crates/ruff_cli/src/resolve.rs +++ b/crates/ruff_cli/src/resolve.rs @@ -4,7 +4,8 @@ use anyhow::Result; use path_absolutize::path_dedot; use ruff::resolver::{ - resolve_settings_with_processor, ConfigProcessor, PyprojectDiscovery, Relativity, + resolve_settings_with_processor, ConfigProcessor, PyprojectConfig, PyprojectDiscoveryStrategy, + Relativity, }; use ruff::settings::configuration::Configuration; use ruff::settings::{pyproject, AllSettings}; @@ -18,13 +19,17 @@ pub fn resolve( config: Option<&Path>, overrides: &Overrides, stdin_filename: Option<&Path>, -) -> Result { +) -> Result { // First priority: if we're running in isolated mode, use the default settings. if isolated { let mut config = Configuration::default(); overrides.process_config(&mut config); let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?; - return Ok(PyprojectDiscovery::Fixed(settings)); + return Ok(PyprojectConfig::new( + PyprojectDiscoveryStrategy::Fixed, + settings, + None, + )); } // Second priority: the user specified a `pyproject.toml` file. Use that @@ -36,7 +41,11 @@ pub fn resolve( .transpose()? { let settings = resolve_settings_with_processor(&pyproject, &Relativity::Cwd, overrides)?; - return Ok(PyprojectDiscovery::Fixed(settings)); + return Ok(PyprojectConfig::new( + PyprojectDiscoveryStrategy::Fixed, + settings, + Some(pyproject), + )); } // Third priority: find a `pyproject.toml` file in either an ancestor of @@ -50,7 +59,11 @@ pub fn resolve( .unwrap_or(&path_dedot::CWD.as_path()), )? { let settings = resolve_settings_with_processor(&pyproject, &Relativity::Parent, overrides)?; - return Ok(PyprojectDiscovery::Hierarchical(settings)); + return Ok(PyprojectConfig::new( + PyprojectDiscoveryStrategy::Hierarchical, + settings, + Some(pyproject), + )); } // Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths @@ -59,7 +72,11 @@ pub fn resolve( // these act as the "default" settings.) if let Some(pyproject) = pyproject::find_user_settings_toml() { let settings = resolve_settings_with_processor(&pyproject, &Relativity::Cwd, overrides)?; - return Ok(PyprojectDiscovery::Hierarchical(settings)); + return Ok(PyprojectConfig::new( + PyprojectDiscoveryStrategy::Hierarchical, + settings, + Some(pyproject), + )); } // Fallback: load Ruff's default settings, and resolve all paths relative to the @@ -69,5 +86,9 @@ pub fn resolve( let mut config = Configuration::default(); overrides.process_config(&mut config); let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?; - Ok(PyprojectDiscovery::Hierarchical(settings)) + Ok(PyprojectConfig::new( + PyprojectDiscoveryStrategy::Hierarchical, + settings, + None, + )) } From 3224615001016e6d333dd7bdb27513de8d80f6a1 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 3 May 2023 17:28:02 +0200 Subject: [PATCH 30/32] Refactor whitespace around operator --- .../missing_whitespace_after_keyword.rs | 5 +- .../missing_whitespace_around_operator.rs | 214 ++++++++++-------- ...ules__pycodestyle__tests__E225_E22.py.snap | 63 +++++- crates/ruff_python_ast/src/token_kind.rs | 28 ++- 4 files changed, 199 insertions(+), 111 deletions(-) diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs index a754becd5445d..dc7a7b9960ccc 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs @@ -22,7 +22,10 @@ pub(crate) fn missing_whitespace_after_keyword( line: &LogicalLine, context: &mut LogicalLinesContext, ) { - for (tok0, tok1) in line.tokens().iter().tuple_windows() { + for window in line.tokens().windows(2) { + let tok0 = &window[0]; + let tok1 = &window[1]; + let tok0_kind = tok0.kind(); let tok1_kind = tok1.kind(); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs index edd064fcd1955..58b6a733bb4d6 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs @@ -1,10 +1,11 @@ use crate::checkers::logical_lines::LogicalLinesContext; -use ruff_diagnostics::Violation; +use itertools::PeekingNext; +use ruff_diagnostics::{DiagnosticKind, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; use ruff_text_size::{TextRange, TextSize}; -use crate::rules::pycodestyle::rules::logical_lines::LogicalLine; +use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineToken}; // E225 #[violation] @@ -56,24 +57,24 @@ pub(crate) fn missing_whitespace_around_operator( line: &LogicalLine, context: &mut LogicalLinesContext, ) { - #[derive(Copy, Clone, Eq, PartialEq)] + #[derive(Copy, Clone, Eq, PartialEq, Debug)] enum NeedsSpace { + /// Needs a leading and trailing space Yes, + /// Doesn't need a leading or trailing space No, - Unset, + /// Needs a trailing space if it has a leading space. + Optional, } - let mut needs_space_main = NeedsSpace::No; - let mut needs_space_aux = NeedsSpace::Unset; - let mut prev_end_aux = TextSize::default(); let mut parens = 0u32; - let mut prev_type: TokenKind = TokenKind::EndOfFile; - let mut prev_end = TextSize::default(); + let mut prev_token: Option<&LogicalLineToken> = None; + let mut tokens = line.tokens().iter().peekable(); - for token in line.tokens() { + while let Some(token) = tokens.next() { let kind = token.kind(); - if kind.is_skip_comment() { + if kind.is_trivia() { continue; } @@ -83,104 +84,123 @@ pub(crate) fn missing_whitespace_around_operator( _ => {} }; - let needs_space = needs_space_main == NeedsSpace::Yes - || needs_space_aux != NeedsSpace::Unset - || prev_end_aux != TextSize::new(0); - if needs_space { - if token.start() > prev_end { - if needs_space_main != NeedsSpace::Yes && needs_space_aux != NeedsSpace::Yes { + let needs_space = if kind == TokenKind::Equal && parens > 0 { + // Allow keyword args or defaults: foo(bar=None). + NeedsSpace::No + } else if kind.is_whitespace_needed() { + NeedsSpace::Yes + } else if kind.is_unary() { + prev_token.map_or(NeedsSpace::No, |prev_token| { + let prev_kind = dbg!(prev_token.kind()); + + // Check if the operator is used as a binary operator + // Allow unary operators: -123, -x, +1. + // Allow argument unpacking: foo(*args, **kwargs) + if matches!( + prev_kind, + TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace + ) || !(prev_kind.is_operator() + || prev_kind.is_keyword() + || prev_kind.is_soft_keyword()) + { + NeedsSpace::Optional + } else { + NeedsSpace::No + } + }) + } else if kind.is_whitespace_optional() { + NeedsSpace::Optional + } else { + NeedsSpace::No + }; + + dbg!(needs_space, kind); + + match needs_space { + NeedsSpace::Yes => { + // Assert leading whitespace + if prev_token.map_or(false, |prev| prev.end() == token.start()) { + // A needed opening space was not found context.push( - MissingWhitespaceAroundOperator, - TextRange::empty(prev_end_aux), + diagnostic_kind_for_operator(kind), + TextRange::empty(token.start()), ); } - needs_space_main = NeedsSpace::No; - needs_space_aux = NeedsSpace::Unset; - prev_end_aux = TextSize::new(0); - } else if kind == TokenKind::Greater - && matches!(prev_type, TokenKind::Less | TokenKind::Minus) - { - // Tolerate the "<>" operator, even if running Python 3 - // Deal with Python 3's annotated return value "->" - } else if prev_type == TokenKind::Slash - && matches!(kind, TokenKind::Comma | TokenKind::Rpar | TokenKind::Colon) - || (prev_type == TokenKind::Rpar && kind == TokenKind::Colon) - { - // Tolerate the "/" operator in function definition - // For more info see PEP570 - } else { - if needs_space_main == NeedsSpace::Yes || needs_space_aux == NeedsSpace::Yes { - context.push(MissingWhitespaceAroundOperator, TextRange::empty(prev_end)); - } else if prev_type != TokenKind::DoubleStar { - if prev_type == TokenKind::Percent { + // Assert trailing whitespace + else if let Some(next_token) = tokens.peek() { + let next_kind = next_token.kind(); + + // Tolerate the "<>" operator, even if running Python 3 + // Deal with Python 3's annotated return value "->" + let not_equal_or_arrow = next_kind == TokenKind::Greater + && matches!(kind, TokenKind::Less | TokenKind::Minus); + + // Tolerate the "/" operator in function definition + // For more info see PEP570 + let is_slash_in_function_definition = matches!( + (kind, next_kind), + ( + TokenKind::Slash, + TokenKind::Comma | TokenKind::Rpar | TokenKind::Colon + ) | (TokenKind::Rpar, TokenKind::Colon) + ); + + let has_trailing_trivia = + next_token.start() > token.end() || next_kind.is_trivia(); + + if !has_trailing_trivia + && !not_equal_or_arrow + && !is_slash_in_function_definition + { context.push( - MissingWhitespaceAroundModuloOperator, - TextRange::empty(prev_end_aux), + diagnostic_kind_for_operator(kind), + TextRange::empty(token.end()), ); - } else if !prev_type.is_arithmetic() { + } + } + } + + NeedsSpace::Optional => { + // Surrounding space is optional, but ensure that + // leading & trailing space matches opening space + let has_leading = prev_token.map_or(false, |prev| prev.end() < token.start()); + let has_trailing = tokens.peek().map_or(false, |next| { + token.end() < next.start() || next.kind().is_trivia() + }); + + // TODO why does this use MissingWhitespaceAroundOperator...always + match (has_leading, has_trailing) { + (true, false) => { context.push( - MissingWhitespaceAroundBitwiseOrShiftOperator, - TextRange::empty(prev_end_aux), + MissingWhitespaceAroundOperator, + TextRange::empty(token.end()), ); - } else { + } + (false, true) => { context.push( - MissingWhitespaceAroundArithmeticOperator, - TextRange::empty(prev_end_aux), + MissingWhitespaceAroundOperator, + TextRange::empty(token.start()), ); } + (false, false) | (true, true) => {} } - needs_space_main = NeedsSpace::No; - needs_space_aux = NeedsSpace::Unset; - prev_end_aux = TextSize::new(0); - } - } else if (kind.is_operator() || matches!(kind, TokenKind::Name)) - && prev_end != TextSize::default() - { - if kind == TokenKind::Equal && parens > 0 { - // Allow keyword args or defaults: foo(bar=None). - } else if kind.is_whitespace_needed() { - needs_space_main = NeedsSpace::Yes; - needs_space_aux = NeedsSpace::Unset; - prev_end_aux = TextSize::new(0); - } else if kind.is_unary() { - // Check if the operator is used as a binary operator - // Allow unary operators: -123, -x, +1. - // Allow argument unpacking: foo(*args, **kwargs) - if (matches!( - prev_type, - TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace - )) || (!prev_type.is_operator() - && !prev_type.is_keyword() - && !prev_type.is_soft_keyword()) - { - needs_space_main = NeedsSpace::Unset; - needs_space_aux = NeedsSpace::Unset; - prev_end_aux = TextSize::new(0); - } - } else if kind.is_whitespace_optional() { - needs_space_main = NeedsSpace::Unset; - needs_space_aux = NeedsSpace::Unset; - prev_end_aux = TextSize::new(0); } - if needs_space_main == NeedsSpace::Unset { - // Surrounding space is optional, but ensure that - // trailing space matches opening space - prev_end_aux = prev_end; - needs_space_aux = if token.start() == prev_end { - NeedsSpace::No - } else { - NeedsSpace::Yes - }; - } else if needs_space_main == NeedsSpace::Yes && token.start() == prev_end_aux { - // A needed opening space was not found - context.push(MissingWhitespaceAroundOperator, TextRange::empty(prev_end)); - needs_space_main = NeedsSpace::No; - needs_space_aux = NeedsSpace::Unset; - prev_end_aux = TextSize::new(0); - } - } - prev_type = kind; - prev_end = token.end(); + NeedsSpace::No => {} + }; + + prev_token = Some(token); + } +} + +fn diagnostic_kind_for_operator(operator: TokenKind) -> DiagnosticKind { + if operator == TokenKind::Percent { + DiagnosticKind::from(MissingWhitespaceAroundModuloOperator) + } else if operator.is_bitwise_or_shift() { + DiagnosticKind::from(MissingWhitespaceAroundBitwiseOrShiftOperator) + } else if operator.is_arithmetic() { + DiagnosticKind::from(MissingWhitespaceAroundArithmeticOperator) + } else { + DiagnosticKind::from(MissingWhitespaceAroundOperator) } } diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap index 0d8ec1b68c0d3..27ece93204aa8 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff/src/rules/pycodestyle/mod.rs +assertion_line: 114 --- E22.py:54:13: E225 Missing whitespace around operator | @@ -10,6 +11,16 @@ E22.py:54:13: E225 Missing whitespace around operator 57 | submitted+= 1 | +E22.py:56:10: E225 Missing whitespace around operator + | +56 | submitted +=1 +57 | #: E225 +58 | submitted+= 1 + | E225 +59 | #: E225 +60 | c =-1 + | + E22.py:58:4: E225 Missing whitespace around operator | 58 | submitted+= 1 @@ -100,12 +111,12 @@ E22.py:74:12: E225 Missing whitespace around operator 78 | i=i+ 1 | -E22.py:76:3: E225 Missing whitespace around operator +E22.py:76:2: E225 Missing whitespace around operator | 76 | _1kB = _1MB>> 10 77 | #: E225 E225 78 | i=i+ 1 - | E225 + | E225 79 | #: E225 E225 80 | i=i +1 | @@ -120,12 +131,12 @@ E22.py:76:4: E225 Missing whitespace around operator 80 | i=i +1 | -E22.py:78:3: E225 Missing whitespace around operator +E22.py:78:2: E225 Missing whitespace around operator | 78 | i=i+ 1 79 | #: E225 E225 80 | i=i +1 - | E225 + | E225 81 | #: E225 82 | i = 1and 1 | @@ -140,6 +151,36 @@ E22.py:78:6: E225 Missing whitespace around operator 82 | i = 1and 1 | +E22.py:80:6: E225 Missing whitespace around operator + | +80 | i=i +1 +81 | #: E225 +82 | i = 1and 1 + | E225 +83 | #: E225 +84 | i = 1or 0 + | + +E22.py:82:6: E225 Missing whitespace around operator + | +82 | i = 1and 1 +83 | #: E225 +84 | i = 1or 0 + | E225 +85 | #: E225 +86 | 1is 1 + | + +E22.py:86:2: E225 Missing whitespace around operator + | +86 | 1is 1 +87 | #: E225 +88 | 1in [] + | E225 +89 | #: E225 +90 | i = 1 @2 + | + E22.py:88:8: E225 Missing whitespace around operator | 88 | 1in [] @@ -160,12 +201,12 @@ E22.py:90:6: E225 Missing whitespace around operator 94 | i=i+1 | -E22.py:92:3: E225 Missing whitespace around operator +E22.py:92:2: E225 Missing whitespace around operator | 92 | i = 1@ 2 93 | #: E225 E226 94 | i=i+1 - | E225 + | E225 95 | #: E225 E226 96 | i =i+1 | @@ -180,6 +221,16 @@ E22.py:94:4: E225 Missing whitespace around operator 98 | i= i+1 | +E22.py:96:2: E225 Missing whitespace around operator + | + 96 | i =i+1 + 97 | #: E225 E226 + 98 | i= i+1 + | E225 + 99 | #: E225 E226 +100 | c = (a +b)*(a - b) + | + E22.py:98:9: E225 Missing whitespace around operator | 98 | i= i+1 diff --git a/crates/ruff_python_ast/src/token_kind.rs b/crates/ruff_python_ast/src/token_kind.rs index 2f44ea2e1ec7f..2dd926cda6261 100644 --- a/crates/ruff_python_ast/src/token_kind.rs +++ b/crates/ruff_python_ast/src/token_kind.rs @@ -195,6 +195,7 @@ impl TokenKind { | TokenKind::In | TokenKind::Is | TokenKind::Rarrow + | TokenKind::Percent ) } @@ -207,7 +208,6 @@ impl TokenKind { | TokenKind::Amper | TokenKind::Vbar | TokenKind::LeftShift - | TokenKind::RightShift | TokenKind::Percent ) } @@ -216,11 +216,7 @@ impl TokenKind { pub const fn is_unary(&self) -> bool { matches!( self, - TokenKind::Plus - | TokenKind::Minus - | TokenKind::Star - | TokenKind::DoubleStar - | TokenKind::RightShift + TokenKind::Plus | TokenKind::Minus | TokenKind::Star | TokenKind::DoubleStar ) } @@ -324,7 +320,7 @@ impl TokenKind { } #[inline] - pub const fn is_skip_comment(&self) -> bool { + pub const fn is_trivia(&self) -> bool { matches!( self, TokenKind::Newline @@ -348,6 +344,24 @@ impl TokenKind { ) } + #[inline] + pub const fn is_bitwise_or_shift(&self) -> bool { + matches!( + self, + TokenKind::LeftShift + | TokenKind::LeftShiftEqual + | TokenKind::RightShift + | TokenKind::RightShiftEqual + | TokenKind::Amper + | TokenKind::AmperEqual + | TokenKind::Vbar + | TokenKind::VbarEqual + | TokenKind::CircumFlex + | TokenKind::CircumflexEqual + | TokenKind::Tilde + ) + } + #[inline] pub const fn is_soft_keyword(&self) -> bool { matches!(self, TokenKind::Match | TokenKind::Case) From 24f81ef30fa5260d58ccc619aeaea8b957ebb065 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 4 May 2023 10:44:51 +0200 Subject: [PATCH 31/32] In progress --- .../test/fixtures/pycodestyle/E22.py | 4 +- .../missing_whitespace_after_keyword.rs | 4 +- .../missing_whitespace_around_operator.rs | 200 ++++++++++-------- ...ules__pycodestyle__tests__E225_E22.py.snap | 21 +- ...ules__pycodestyle__tests__E226_E22.py.snap | 19 ++ crates/ruff_python_ast/src/token_kind.rs | 56 +---- 6 files changed, 157 insertions(+), 147 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/E22.py b/crates/ruff/resources/test/fixtures/pycodestyle/E22.py index 7ea27927e50fe..6737d2ce2f622 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/E22.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/E22.py @@ -151,8 +151,8 @@ def halves(n): func1(lambda *args, **kw: (args, kw)) func2(lambda a, b=h[:], c=0: (a, b, c)) if not -5 < x < +5: - print >>sys.stderr, "x is out of range." -print >> sys.stdout, "x is an integer." + print >> sys.stderr, "x is out of range." +print >>sys.stdout, "x is an integer." x = x / 2 - 1 x = 1 @ 2 diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs index dc7a7b9960ccc..6460713f55960 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs @@ -1,11 +1,9 @@ -use itertools::Itertools; -use ruff_text_size::TextRange; - use crate::checkers::logical_lines::LogicalLinesContext; use crate::rules::pycodestyle::rules::logical_lines::LogicalLine; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; +use ruff_text_size::TextRange; #[violation] pub struct MissingWhitespaceAfterKeyword; diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs index 58b6a733bb4d6..071c0a9dc4693 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs @@ -1,11 +1,9 @@ use crate::checkers::logical_lines::LogicalLinesContext; -use itertools::PeekingNext; +use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineToken}; use ruff_diagnostics::{DiagnosticKind, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use ruff_text_size::{TextRange, TextSize}; - -use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineToken}; +use ruff_text_size::TextRange; // E225 #[violation] @@ -57,16 +55,6 @@ pub(crate) fn missing_whitespace_around_operator( line: &LogicalLine, context: &mut LogicalLinesContext, ) { - #[derive(Copy, Clone, Eq, PartialEq, Debug)] - enum NeedsSpace { - /// Needs a leading and trailing space - Yes, - /// Doesn't need a leading or trailing space - No, - /// Needs a trailing space if it has a leading space. - Optional, - } - let mut parens = 0u32; let mut prev_token: Option<&LogicalLineToken> = None; let mut tokens = line.tokens().iter().peekable(); @@ -80,119 +68,123 @@ pub(crate) fn missing_whitespace_around_operator( match kind { TokenKind::Lpar | TokenKind::Lambda => parens += 1, - TokenKind::Rpar => parens -= 1, + TokenKind::Rpar => parens = parens.saturating_sub(1), _ => {} }; let needs_space = if kind == TokenKind::Equal && parens > 0 { // Allow keyword args or defaults: foo(bar=None). NeedsSpace::No - } else if kind.is_whitespace_needed() { - NeedsSpace::Yes - } else if kind.is_unary() { + } else if kind == TokenKind::Slash { + // Tolerate the "/" operator in function definition + // For more info see PEP570 + + // `def f(a, /, b):` or `def f(a, b, /):` or `f = lambda a, /:` + // ^ ^ ^ + let slash_in_func = matches!( + tokens.peek().map(|t| t.kind()), + Some(TokenKind::Comma | TokenKind::Rpar | TokenKind::Colon) + ); + + NeedsSpace::from(!slash_in_func) + } else if kind.is_unary() || kind == TokenKind::DoubleStar { prev_token.map_or(NeedsSpace::No, |prev_token| { - let prev_kind = dbg!(prev_token.kind()); + let prev_kind = prev_token.kind(); - // Check if the operator is used as a binary operator + // Check if the operator is used as a binary operator. // Allow unary operators: -123, -x, +1. // Allow argument unpacking: foo(*args, **kwargs) - if matches!( + let is_binary = matches!( prev_kind, TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace ) || !(prev_kind.is_operator() || prev_kind.is_keyword() - || prev_kind.is_soft_keyword()) - { - NeedsSpace::Optional + || prev_kind.is_soft_keyword()); + + if is_binary { + if kind == TokenKind::DoubleStar { + // Enforce consistent spacing, but don't enforce whitespaces. + NeedsSpace::Optional + } else { + NeedsSpace::Yes + } } else { NeedsSpace::No } }) - } else if kind.is_whitespace_optional() { - NeedsSpace::Optional + } else if is_whitespace_needed(kind) { + NeedsSpace::Yes } else { NeedsSpace::No }; - dbg!(needs_space, kind); + if needs_space != NeedsSpace::No { + let has_leading_trivia = prev_token.map_or(true, |prev| { + prev.end() < token.start() || prev.kind().is_trivia() + }); + + let has_trailing_trivia = tokens.peek().map_or(true, |next| { + token.end() < next.start() || next.kind().is_trivia() + }); - match needs_space { - NeedsSpace::Yes => { - // Assert leading whitespace - if prev_token.map_or(false, |prev| prev.end() == token.start()) { - // A needed opening space was not found + match (has_leading_trivia, has_trailing_trivia) { + // Operator with trailing but no leading space, enforce consistent spacing + (false, true) => { context.push( - diagnostic_kind_for_operator(kind), + MissingWhitespaceAroundOperator, TextRange::empty(token.start()), ); } - // Assert trailing whitespace - else if let Some(next_token) = tokens.peek() { - let next_kind = next_token.kind(); - - // Tolerate the "<>" operator, even if running Python 3 - // Deal with Python 3's annotated return value "->" - let not_equal_or_arrow = next_kind == TokenKind::Greater - && matches!(kind, TokenKind::Less | TokenKind::Minus); - - // Tolerate the "/" operator in function definition - // For more info see PEP570 - let is_slash_in_function_definition = matches!( - (kind, next_kind), - ( - TokenKind::Slash, - TokenKind::Comma | TokenKind::Rpar | TokenKind::Colon - ) | (TokenKind::Rpar, TokenKind::Colon) + // Operator with leading but no trailing space, enforce consistent spacing. + (true, false) => { + context.push( + MissingWhitespaceAroundOperator, + TextRange::empty(token.end()), ); - - let has_trailing_trivia = - next_token.start() > token.end() || next_kind.is_trivia(); - - if !has_trailing_trivia - && !not_equal_or_arrow - && !is_slash_in_function_definition - { - context.push( - diagnostic_kind_for_operator(kind), - TextRange::empty(token.end()), - ); - } } - } - - NeedsSpace::Optional => { - // Surrounding space is optional, but ensure that - // leading & trailing space matches opening space - let has_leading = prev_token.map_or(false, |prev| prev.end() < token.start()); - let has_trailing = tokens.peek().map_or(false, |next| { - token.end() < next.start() || next.kind().is_trivia() - }); - - // TODO why does this use MissingWhitespaceAroundOperator...always - match (has_leading, has_trailing) { - (true, false) => { + // Operator with no space, require spaces if it is required by the operator. + (false, false) => { + if needs_space == NeedsSpace::Yes { context.push( - MissingWhitespaceAroundOperator, - TextRange::empty(token.end()), - ); - } - (false, true) => { - context.push( - MissingWhitespaceAroundOperator, + diagnostic_kind_for_operator(kind), TextRange::empty(token.start()), ); } - (false, false) | (true, true) => {} + } + (true, true) => { + // Operator has leading and trailing space, all good } } - - NeedsSpace::No => {} - }; + } prev_token = Some(token); } } +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +enum NeedsSpace { + /// Needs a leading and trailing space. + Yes, + + /// Doesn't need a leading or trailing space. Or in other words, we don't care how many + /// leading or trailing spaces that token has. + No, + + /// Needs consistent leading and trailing spacing. The operator needs spacing if + /// * it has a leading space + /// * it has a trailing space + Optional, +} + +impl From for NeedsSpace { + fn from(value: bool) -> Self { + match value { + true => NeedsSpace::Yes, + false => NeedsSpace::No, + } + } +} + fn diagnostic_kind_for_operator(operator: TokenKind) -> DiagnosticKind { if operator == TokenKind::Percent { DiagnosticKind::from(MissingWhitespaceAroundModuloOperator) @@ -204,3 +196,37 @@ fn diagnostic_kind_for_operator(operator: TokenKind) -> DiagnosticKind { DiagnosticKind::from(MissingWhitespaceAroundOperator) } } + +fn is_whitespace_needed(kind: TokenKind) -> bool { + matches!( + kind, + TokenKind::DoubleStarEqual + | TokenKind::StarEqual + | TokenKind::SlashEqual + | TokenKind::DoubleSlashEqual + | TokenKind::PlusEqual + | TokenKind::MinusEqual + | TokenKind::NotEqual + | TokenKind::Less + | TokenKind::Greater + | TokenKind::PercentEqual + | TokenKind::CircumflexEqual + | TokenKind::AmperEqual + | TokenKind::VbarEqual + | TokenKind::EqEqual + | TokenKind::LessEqual + | TokenKind::GreaterEqual + | TokenKind::LeftShiftEqual + | TokenKind::RightShiftEqual + | TokenKind::Equal + | TokenKind::And + | TokenKind::Or + | TokenKind::In + | TokenKind::Is + | TokenKind::Rarrow + | TokenKind::ColonEqual + | TokenKind::Slash + | TokenKind::Percent + ) || kind.is_arithmetic() + || kind.is_bitwise_or_shift() +} diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap index 27ece93204aa8..f8979efb10782 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff/src/rules/pycodestyle/mod.rs -assertion_line: 114 --- E22.py:54:13: E225 Missing whitespace around operator | @@ -171,6 +170,16 @@ E22.py:82:6: E225 Missing whitespace around operator 86 | 1is 1 | +E22.py:84:2: E225 Missing whitespace around operator + | +84 | i = 1or 0 +85 | #: E225 +86 | 1is 1 + | E225 +87 | #: E225 +88 | 1in [] + | + E22.py:86:2: E225 Missing whitespace around operator | 86 | 1is 1 @@ -250,14 +259,14 @@ E22.py:100:7: E225 Missing whitespace around operator 103 | #: | -E22.py:154:13: E225 Missing whitespace around operator +E22.py:155:9: E225 Missing whitespace around operator | -154 | func2(lambda a, b=h[:], c=0: (a, b, c)) 155 | if not -5 < x < +5: -156 | print >>sys.stderr, "x is out of range." - | E225 -157 | print >> sys.stdout, "x is an integer." +156 | print >> sys.stderr, "x is out of range." +157 | print >>sys.stdout, "x is an integer." + | E225 158 | x = x / 2 - 1 +159 | x = 1 @ 2 | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap index c13c83a120483..769264c53d61a 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap @@ -50,6 +50,15 @@ E22.py:100:11: E226 Missing whitespace around arithmetic operator 103 | #: | +E22.py:104:6: E226 Missing whitespace around arithmetic operator + | +104 | #: E226 +105 | z = 2//30 + | E226 +106 | #: E226 E226 +107 | c = (a+b) * (a-b) + | + E22.py:106:7: E226 Missing whitespace around arithmetic operator | 106 | z = 2//30 @@ -120,4 +129,14 @@ E22.py:116:12: E226 Missing whitespace around arithmetic operator 120 | def halves(n): | +E22.py:119:14: E226 Missing whitespace around arithmetic operator + | +119 | #: E226 +120 | def halves(n): +121 | return (i//2 for i in range(n)) + | E226 +122 | #: E227 +123 | _1kB = _1MB>>10 + | + diff --git a/crates/ruff_python_ast/src/token_kind.rs b/crates/ruff_python_ast/src/token_kind.rs index 2dd926cda6261..b465e39860824 100644 --- a/crates/ruff_python_ast/src/token_kind.rs +++ b/crates/ruff_python_ast/src/token_kind.rs @@ -167,57 +167,9 @@ pub enum TokenKind { } impl TokenKind { - #[inline] - pub const fn is_whitespace_needed(&self) -> bool { - matches!( - self, - TokenKind::DoubleStarEqual - | TokenKind::StarEqual - | TokenKind::SlashEqual - | TokenKind::DoubleSlashEqual - | TokenKind::PlusEqual - | TokenKind::MinusEqual - | TokenKind::NotEqual - | TokenKind::Less - | TokenKind::Greater - | TokenKind::PercentEqual - | TokenKind::CircumflexEqual - | TokenKind::AmperEqual - | TokenKind::VbarEqual - | TokenKind::EqEqual - | TokenKind::LessEqual - | TokenKind::GreaterEqual - | TokenKind::LeftShiftEqual - | TokenKind::RightShiftEqual - | TokenKind::Equal - | TokenKind::And - | TokenKind::Or - | TokenKind::In - | TokenKind::Is - | TokenKind::Rarrow - | TokenKind::Percent - ) - } - - #[inline] - pub const fn is_whitespace_optional(&self) -> bool { - self.is_arithmetic() - || matches!( - self, - TokenKind::CircumFlex - | TokenKind::Amper - | TokenKind::Vbar - | TokenKind::LeftShift - | TokenKind::Percent - ) - } - #[inline] pub const fn is_unary(&self) -> bool { - matches!( - self, - TokenKind::Plus | TokenKind::Minus | TokenKind::Star | TokenKind::DoubleStar - ) + matches!(self, TokenKind::Plus | TokenKind::Minus | TokenKind::Star) } #[inline] @@ -311,6 +263,11 @@ impl TokenKind { | TokenKind::Ellipsis | TokenKind::ColonEqual | TokenKind::Colon + | TokenKind::And + | TokenKind::Or + | TokenKind::Not + | TokenKind::In + | TokenKind::Is ) } @@ -340,6 +297,7 @@ impl TokenKind { | TokenKind::Plus | TokenKind::Minus | TokenKind::Slash + | TokenKind::DoubleSlash | TokenKind::At ) } From 11c11f324b3cf7fc4e8620eec07a08dc08b26d29 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 27 Apr 2023 07:34:20 -0700 Subject: [PATCH 32/32] Use non-empty ranges for logical-lines diagnostics --- .../logical_lines/extraneous_whitespace.rs | 21 +++-- .../rules/logical_lines/missing_whitespace.rs | 5 +- .../missing_whitespace_after_keyword.rs | 2 +- .../missing_whitespace_around_operator.rs | 23 ++---- .../pycodestyle/rules/logical_lines/mod.rs | 20 +++-- .../logical_lines/space_around_operator.rs | 19 +++-- .../whitespace_around_keywords.rs | 14 ++-- ...hitespace_around_named_parameter_equals.rs | 16 ++-- ...ules__pycodestyle__tests__E201_E20.py.snap | 12 +-- ...ules__pycodestyle__tests__E202_E20.py.snap | 12 +-- ...ules__pycodestyle__tests__E203_E20.py.snap | 12 +-- ...ules__pycodestyle__tests__E221_E22.py.snap | 16 ++-- ...ules__pycodestyle__tests__E222_E22.py.snap | 10 +-- ...ules__pycodestyle__tests__E223_E22.py.snap | 2 +- ...ules__pycodestyle__tests__E224_E22.py.snap | 2 +- ...ules__pycodestyle__tests__E225_E22.py.snap | 78 +++++++++---------- ...ules__pycodestyle__tests__E226_E22.py.snap | 28 +++---- ...ules__pycodestyle__tests__E227_E22.py.snap | 10 +-- ...ules__pycodestyle__tests__E228_E22.py.snap | 6 +- ...ules__pycodestyle__tests__E231_E23.py.snap | 8 +- ...ules__pycodestyle__tests__E251_E25.py.snap | 46 ++++++----- ...ules__pycodestyle__tests__E252_E25.py.snap | 12 +-- ...ules__pycodestyle__tests__E271_E27.py.snap | 18 ++--- ...ules__pycodestyle__tests__E272_E27.py.snap | 6 +- ...ules__pycodestyle__tests__E273_E27.py.snap | 10 +-- ...ules__pycodestyle__tests__E274_E27.py.snap | 4 +- ...ules__pycodestyle__tests__E275_E27.py.snap | 20 ++--- 27 files changed, 214 insertions(+), 218 deletions(-) diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs index 96e5a2a580fb6..53fd590e5c9ed 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs @@ -109,8 +109,12 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin let kind = token.kind(); match kind { TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => { - if !matches!(line.trailing_whitespace(token), Whitespace::None) { - context.push(WhitespaceAfterOpenBracket, TextRange::empty(token.end())); + let (trailing, trailing_len) = line.trailing_whitespace(token); + if !matches!(trailing, Whitespace::None) { + context.push( + WhitespaceAfterOpenBracket, + TextRange::at(token.end(), trailing_len), + ); } } TokenKind::Rbrace @@ -119,10 +123,10 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin | TokenKind::Comma | TokenKind::Semi | TokenKind::Colon => { - if let (Whitespace::Single | Whitespace::Many | Whitespace::Tab, offset) = - line.leading_whitespace(token) - { - if !matches!(last_token, TokenKind::Comma | TokenKind::EndOfFile) { + if !matches!(last_token, TokenKind::Comma | TokenKind::EndOfFile) { + if let (Whitespace::Single | Whitespace::Many | Whitespace::Tab, offset) = + line.leading_whitespace(token) + { let diagnostic_kind = if matches!( kind, TokenKind::Comma | TokenKind::Semi | TokenKind::Colon @@ -132,7 +136,10 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin DiagnosticKind::from(WhitespaceBeforeCloseBracket) }; - context.push(diagnostic_kind, TextRange::empty(token.start() - offset)); + context.push( + diagnostic_kind, + TextRange::at(token.start() - offset, offset), + ); } } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs index 56125881046c8..1bd0e492a45dc 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs @@ -4,7 +4,7 @@ use ruff_diagnostics::Edit; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::TextSize; #[violation] pub struct MissingWhitespace { @@ -82,8 +82,7 @@ pub(crate) fn missing_whitespace( } let kind = MissingWhitespace { token: kind }; - - let mut diagnostic = Diagnostic::new(kind, TextRange::empty(token.start())); + let mut diagnostic = Diagnostic::new(kind, token.range()); if autofix { diagnostic.set_fix(Edit::insertion(" ".to_string(), token.end())); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs index 6460713f55960..e80d31d011ca6 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs @@ -35,7 +35,7 @@ pub(crate) fn missing_whitespace_after_keyword( || matches!(tok1_kind, TokenKind::Colon | TokenKind::Newline)) && tok0.end() == tok1.start() { - context.push(MissingWhitespaceAfterKeyword, TextRange::empty(tok0.end())); + context.push(MissingWhitespaceAfterKeyword, tok0.range()); } } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs index 071c0a9dc4693..ee05e638b7c77 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs @@ -3,7 +3,6 @@ use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineTo use ruff_diagnostics::{DiagnosticKind, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use ruff_text_size::TextRange; // E225 #[violation] @@ -129,30 +128,20 @@ pub(crate) fn missing_whitespace_around_operator( match (has_leading_trivia, has_trailing_trivia) { // Operator with trailing but no leading space, enforce consistent spacing - (false, true) => { - context.push( - MissingWhitespaceAroundOperator, - TextRange::empty(token.start()), - ); - } + (false, true) | // Operator with leading but no trailing space, enforce consistent spacing. - (true, false) => { - context.push( - MissingWhitespaceAroundOperator, - TextRange::empty(token.end()), - ); + (true, false) + => { + context.push(MissingWhitespaceAroundOperator, token.range()); } // Operator with no space, require spaces if it is required by the operator. (false, false) => { if needs_space == NeedsSpace::Yes { - context.push( - diagnostic_kind_for_operator(kind), - TextRange::empty(token.start()), - ); + context.push(diagnostic_kind_for_operator(kind), token.range()); } } (true, true) => { - // Operator has leading and trailing space, all good + // Operator has leading and trailing spaces, all good } } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs index b98aeceaf24d1..31ff9b173b66f 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -230,6 +230,10 @@ impl<'a> LogicalLine<'a> { .slice(TextRange::new(token.end(), last_token.end())) } + pub fn token_text(&self, token: &'a LogicalLineToken) -> &str { + self.lines.locator.slice(token.range) + } + /// Returns the text before `token` #[inline] pub fn text_before(&self, token: &'a LogicalLineToken) -> &str { @@ -240,8 +244,8 @@ impl<'a> LogicalLine<'a> { .slice(TextRange::new(first_token.start(), token.start())) } - /// Returns the whitespace *after* the `token` - pub fn trailing_whitespace(&self, token: &'a LogicalLineToken) -> Whitespace { + /// Returns the whitespace *after* the `token` with the byte length + pub fn trailing_whitespace(&self, token: &'a LogicalLineToken) -> (Whitespace, TextSize) { Whitespace::leading(self.text_after(token)) } @@ -358,25 +362,27 @@ pub(crate) enum Whitespace { } impl Whitespace { - fn leading(content: &str) -> Self { + fn leading(content: &str) -> (Self, TextSize) { let mut count = 0u32; + let mut len = TextSize::default(); for c in content.chars() { if c == '\t' { - return Self::Tab; + return (Self::Tab, len + c.text_len()); } else if matches!(c, '\n' | '\r') { break; } else if c.is_whitespace() { count += 1; + len += c.text_len(); } else { break; } } match count { - 0 => Whitespace::None, - 1 => Whitespace::Single, - _ => Whitespace::Many, + 0 => (Whitespace::None, len), + 1 => (Whitespace::Single, len), + _ => (Whitespace::Many, len), } } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs index e3cba4c894bd9..beb2e9552ef6f 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs @@ -133,14 +133,15 @@ pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLin if !after_operator { match line.leading_whitespace(token) { (Whitespace::Tab, offset) => { - let start = token.start(); - context.push(TabBeforeOperator, TextRange::empty(start - offset)); + context.push( + TabBeforeOperator, + TextRange::at(token.start() - offset, offset), + ); } (Whitespace::Many, offset) => { - let start = token.start(); context.push( MultipleSpacesBeforeOperator, - TextRange::empty(start - offset), + TextRange::at(token.start() - offset, offset), ); } _ => {} @@ -148,13 +149,11 @@ pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLin } match line.trailing_whitespace(token) { - Whitespace::Tab => { - let end = token.end(); - context.push(TabAfterOperator, TextRange::empty(end)); + (Whitespace::Tab, len) => { + context.push(TabAfterOperator, TextRange::at(token.end(), len)); } - Whitespace::Many => { - let end = token.end(); - context.push(MultipleSpacesAfterOperator, TextRange::empty(end)); + (Whitespace::Many, len) => { + context.push(MultipleSpacesAfterOperator, TextRange::at(token.end(), len)); } _ => {} } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs index 121fee2659544..a181c925a87aa 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs @@ -118,13 +118,13 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic match line.leading_whitespace(token) { (Whitespace::Tab, offset) => { let start = token.start(); - context.push(TabBeforeKeyword, TextRange::empty(start - offset)); + context.push(TabBeforeKeyword, TextRange::at(start - offset, offset)); } (Whitespace::Many, offset) => { let start = token.start(); context.push( MultipleSpacesBeforeKeyword, - TextRange::empty(start - offset), + TextRange::at(start - offset, offset), ); } _ => {} @@ -132,13 +132,11 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic } match line.trailing_whitespace(token) { - Whitespace::Tab => { - let end = token.end(); - context.push(TabAfterKeyword, TextRange::empty(end)); + (Whitespace::Tab, len) => { + context.push(TabAfterKeyword, TextRange::at(token.end(), len)); } - Whitespace::Many => { - let end = token.end(); - context.push(MultipleSpacesAfterKeyword, TextRange::empty(end)); + (Whitespace::Many, len) => { + context.push(MultipleSpacesAfterKeyword, TextRange::at(token.end(), len)); } _ => {} } diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs index bf104e71799d0..e65f610fffcdd 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs @@ -3,7 +3,7 @@ use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineTo use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::token_kind::TokenKind; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange, TextSize}; #[violation] pub struct UnexpectedSpacesAroundKeywordParameterEquals; @@ -78,10 +78,7 @@ pub(crate) fn whitespace_around_named_parameter_equals( if annotated_func_arg && parens == 1 { let start = token.start(); if start == prev_end && prev_end != TextSize::new(0) { - context.push( - MissingWhitespaceAroundParameterEquals, - TextRange::empty(start), - ); + context.push(MissingWhitespaceAroundParameterEquals, token.range()); } while let Some(next) = iter.peek() { @@ -91,10 +88,7 @@ pub(crate) fn whitespace_around_named_parameter_equals( let next_start = next.start(); if next_start == token.end() { - context.push( - MissingWhitespaceAroundParameterEquals, - TextRange::empty(next_start), - ); + context.push(MissingWhitespaceAroundParameterEquals, token.range()); } break; } @@ -103,7 +97,7 @@ pub(crate) fn whitespace_around_named_parameter_equals( if token.start() != prev_end { context.push( UnexpectedSpacesAroundKeywordParameterEquals, - TextRange::empty(prev_end), + TextRange::new(prev_end, token.start()), ); } @@ -114,7 +108,7 @@ pub(crate) fn whitespace_around_named_parameter_equals( if next.start() != token.end() { context.push( UnexpectedSpacesAroundKeywordParameterEquals, - TextRange::empty(token.end()), + TextRange::new(token.end(), next.start()), ); } break; diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap index 198479926f3aa..803111beb394a 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap @@ -5,7 +5,7 @@ E20.py:2:6: E201 Whitespace after '(' | 2 | #: E201:1:6 3 | spam( ham[1], {eggs: 2}) - | E201 + | ^ E201 4 | #: E201:1:10 5 | spam(ham[ 1], {eggs: 2}) | @@ -15,7 +15,7 @@ E20.py:4:10: E201 Whitespace after '(' 4 | spam( ham[1], {eggs: 2}) 5 | #: E201:1:10 6 | spam(ham[ 1], {eggs: 2}) - | E201 + | ^ E201 7 | #: E201:1:15 8 | spam(ham[1], { eggs: 2}) | @@ -25,7 +25,7 @@ E20.py:6:15: E201 Whitespace after '(' 6 | spam(ham[ 1], {eggs: 2}) 7 | #: E201:1:15 8 | spam(ham[1], { eggs: 2}) - | E201 + | ^ E201 9 | #: E201:1:6 10 | spam( ham[1], {eggs: 2}) | @@ -35,7 +35,7 @@ E20.py:8:6: E201 Whitespace after '(' 8 | spam(ham[1], { eggs: 2}) 9 | #: E201:1:6 10 | spam( ham[1], {eggs: 2}) - | E201 + | ^^^^ E201 11 | #: E201:1:10 12 | spam(ham[ 1], {eggs: 2}) | @@ -45,7 +45,7 @@ E20.py:10:10: E201 Whitespace after '(' 10 | spam( ham[1], {eggs: 2}) 11 | #: E201:1:10 12 | spam(ham[ 1], {eggs: 2}) - | E201 + | ^^^^ E201 13 | #: E201:1:15 14 | spam(ham[1], { eggs: 2}) | @@ -55,7 +55,7 @@ E20.py:12:15: E201 Whitespace after '(' 12 | spam(ham[ 1], {eggs: 2}) 13 | #: E201:1:15 14 | spam(ham[1], { eggs: 2}) - | E201 + | ^^^^ E201 15 | #: Okay 16 | spam(ham[1], {eggs: 2}) | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap index 1215dbb53bb98..4d19157fda28b 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap @@ -5,7 +5,7 @@ E20.py:19:23: E202 Whitespace before ')' | 19 | #: E202:1:23 20 | spam(ham[1], {eggs: 2} ) - | E202 + | ^ E202 21 | #: E202:1:22 22 | spam(ham[1], {eggs: 2 }) | @@ -15,7 +15,7 @@ E20.py:21:22: E202 Whitespace before ')' 21 | spam(ham[1], {eggs: 2} ) 22 | #: E202:1:22 23 | spam(ham[1], {eggs: 2 }) - | E202 + | ^ E202 24 | #: E202:1:11 25 | spam(ham[1 ], {eggs: 2}) | @@ -25,7 +25,7 @@ E20.py:23:11: E202 Whitespace before ')' 23 | spam(ham[1], {eggs: 2 }) 24 | #: E202:1:11 25 | spam(ham[1 ], {eggs: 2}) - | E202 + | ^ E202 26 | #: E202:1:23 27 | spam(ham[1], {eggs: 2} ) | @@ -35,7 +35,7 @@ E20.py:25:23: E202 Whitespace before ')' 25 | spam(ham[1 ], {eggs: 2}) 26 | #: E202:1:23 27 | spam(ham[1], {eggs: 2} ) - | E202 + | ^^^^ E202 28 | #: E202:1:22 29 | spam(ham[1], {eggs: 2 }) | @@ -45,7 +45,7 @@ E20.py:27:22: E202 Whitespace before ')' 27 | spam(ham[1], {eggs: 2} ) 28 | #: E202:1:22 29 | spam(ham[1], {eggs: 2 }) - | E202 + | ^^^^ E202 30 | #: E202:1:11 31 | spam(ham[1 ], {eggs: 2}) | @@ -55,7 +55,7 @@ E20.py:29:11: E202 Whitespace before ')' 29 | spam(ham[1], {eggs: 2 }) 30 | #: E202:1:11 31 | spam(ham[1 ], {eggs: 2}) - | E202 + | ^^^^ E202 32 | #: Okay 33 | spam(ham[1], {eggs: 2}) | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap index b59e240bd7f46..d2dd346b43d83 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap @@ -5,7 +5,7 @@ E20.py:51:10: E203 Whitespace before ',', ';', or ':' | 51 | #: E203:1:10 52 | if x == 4 : - | E203 + | ^ E203 53 | print x, y 54 | x, y = y, x | @@ -15,7 +15,7 @@ E20.py:55:10: E203 Whitespace before ',', ';', or ':' 55 | x, y = y, x 56 | #: E203:1:10 57 | if x == 4 : - | E203 + | ^^^^ E203 58 | print x, y 59 | x, y = y, x | @@ -25,7 +25,7 @@ E20.py:60:15: E203 Whitespace before ',', ';', or ':' 60 | #: E203:2:15 E702:2:16 61 | if x == 4: 62 | print x, y ; x, y = y, x - | E203 + | ^ E203 63 | #: E203:2:15 E702:2:16 64 | if x == 4: | @@ -35,7 +35,7 @@ E20.py:63:15: E203 Whitespace before ',', ';', or ':' 63 | #: E203:2:15 E702:2:16 64 | if x == 4: 65 | print x, y ; x, y = y, x - | E203 + | ^^^^ E203 66 | #: E203:3:13 67 | if x == 4: | @@ -45,7 +45,7 @@ E20.py:67:13: E203 Whitespace before ',', ';', or ':' 67 | if x == 4: 68 | print x, y 69 | x, y = y , x - | E203 + | ^ E203 70 | #: E203:3:13 71 | if x == 4: | @@ -55,7 +55,7 @@ E20.py:71:13: E203 Whitespace before ',', ';', or ':' 71 | if x == 4: 72 | print x, y 73 | x, y = y , x - | E203 + | ^^^^ E203 74 | #: Okay 75 | if x == 4: | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E221_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E221_E22.py.snap index 2dd5e63fd6e2e..29c9bad92e079 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E221_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E221_E22.py.snap @@ -6,7 +6,7 @@ E22.py:3:6: E221 Multiple spaces before operator 3 | #: E221 4 | a = 12 + 3 5 | b = 4 + 5 - | E221 + | ^^ E221 6 | #: E221 E221 7 | x = 1 | @@ -16,7 +16,7 @@ E22.py:5:2: E221 Multiple spaces before operator 5 | b = 4 + 5 6 | #: E221 E221 7 | x = 1 - | E221 + | ^^^^^^^^^^^^^ E221 8 | y = 2 9 | long_variable = 3 | @@ -26,7 +26,7 @@ E22.py:6:2: E221 Multiple spaces before operator 6 | #: E221 E221 7 | x = 1 8 | y = 2 - | E221 + | ^^^^^^^^^^^^^ E221 9 | long_variable = 3 10 | #: E221 E221 | @@ -36,7 +36,7 @@ E22.py:9:5: E221 Multiple spaces before operator 9 | long_variable = 3 10 | #: E221 E221 11 | x[0] = 1 - | E221 + | ^^^^^^^^^^ E221 12 | x[1] = 2 13 | long_variable = 3 | @@ -46,7 +46,7 @@ E22.py:10:5: E221 Multiple spaces before operator 10 | #: E221 E221 11 | x[0] = 1 12 | x[1] = 2 - | E221 + | ^^^^^^^^^^ E221 13 | long_variable = 3 14 | #: E221 E221 | @@ -56,7 +56,7 @@ E22.py:13:9: E221 Multiple spaces before operator 13 | long_variable = 3 14 | #: E221 E221 15 | x = f(x) + 1 - | E221 + | ^^^^^^^^^^ E221 16 | y = long_variable + 2 17 | z = x[0] + 3 | @@ -66,7 +66,7 @@ E22.py:15:9: E221 Multiple spaces before operator 15 | x = f(x) + 1 16 | y = long_variable + 2 17 | z = x[0] + 3 - | E221 + | ^^^^^^^^^^ E221 18 | #: E221:3:14 19 | text = """ | @@ -76,7 +76,7 @@ E22.py:19:14: E221 Multiple spaces before operator 19 | text = """ 20 | bar 21 | foo %s""" % rofl - | E221 + | ^^ E221 22 | #: Okay 23 | x = 1 | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E222_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E222_E22.py.snap index e3e4d8d9aebe5..0f4ded92a73f1 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E222_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E222_E22.py.snap @@ -5,7 +5,7 @@ E22.py:28:8: E222 Multiple spaces after operator | 28 | #: E222 29 | a = a + 1 - | E222 + | ^^ E222 30 | b = b + 10 31 | #: E222 E222 | @@ -15,7 +15,7 @@ E22.py:31:4: E222 Multiple spaces after operator 31 | b = b + 10 32 | #: E222 E222 33 | x = -1 - | E222 + | ^^^^^^^^^^^^ E222 34 | y = -2 35 | long_variable = 3 | @@ -25,7 +25,7 @@ E22.py:32:4: E222 Multiple spaces after operator 32 | #: E222 E222 33 | x = -1 34 | y = -2 - | E222 + | ^^^^^^^^^^^^ E222 35 | long_variable = 3 36 | #: E222 E222 | @@ -35,7 +35,7 @@ E22.py:35:7: E222 Multiple spaces after operator 35 | long_variable = 3 36 | #: E222 E222 37 | x[0] = 1 - | E222 + | ^^^^^^^^^^ E222 38 | x[1] = 2 39 | long_variable = 3 | @@ -45,7 +45,7 @@ E22.py:36:7: E222 Multiple spaces after operator 36 | #: E222 E222 37 | x[0] = 1 38 | x[1] = 2 - | E222 + | ^^^^^^^^^^ E222 39 | long_variable = 3 40 | #: | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap index 0e7405603f814..d8ca6d3deef0f 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap @@ -6,7 +6,7 @@ E22.py:43:2: E223 Tab before operator 43 | #: E223 44 | foobart = 4 45 | a = 3 # aligned with tab - | E223 + | ^^^^ E223 46 | #: | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap index a8344802ea318..a6a0b44367381 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap @@ -5,7 +5,7 @@ E22.py:48:5: E224 Tab after operator | 48 | #: E224 49 | a += 1 - | E224 + | ^^^^ E224 50 | b += 1000 51 | #: | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap index f8979efb10782..2deb56612f295 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E225_E22.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/pycodestyle/mod.rs --- -E22.py:54:13: E225 Missing whitespace around operator +E22.py:54:11: E225 Missing whitespace around operator | 54 | #: E225 55 | submitted +=1 - | E225 + | ^^ E225 56 | #: E225 57 | submitted+= 1 | @@ -15,37 +15,37 @@ E22.py:56:10: E225 Missing whitespace around operator 56 | submitted +=1 57 | #: E225 58 | submitted+= 1 - | E225 + | ^^ E225 59 | #: E225 60 | c =-1 | -E22.py:58:4: E225 Missing whitespace around operator +E22.py:58:3: E225 Missing whitespace around operator | 58 | submitted+= 1 59 | #: E225 60 | c =-1 - | E225 + | ^ E225 61 | #: E225 62 | x = x /2 - 1 | -E22.py:60:8: E225 Missing whitespace around operator +E22.py:60:7: E225 Missing whitespace around operator | 60 | c =-1 61 | #: E225 62 | x = x /2 - 1 - | E225 + | ^ E225 63 | #: E225 64 | c = alpha -4 | -E22.py:62:12: E225 Missing whitespace around operator +E22.py:62:11: E225 Missing whitespace around operator | 62 | x = x /2 - 1 63 | #: E225 64 | c = alpha -4 - | E225 + | ^ E225 65 | #: E225 66 | c = alpha- 4 | @@ -55,27 +55,27 @@ E22.py:64:10: E225 Missing whitespace around operator 64 | c = alpha -4 65 | #: E225 66 | c = alpha- 4 - | E225 + | ^ E225 67 | #: E225 68 | z = x **y | -E22.py:66:9: E225 Missing whitespace around operator +E22.py:66:7: E225 Missing whitespace around operator | 66 | c = alpha- 4 67 | #: E225 68 | z = x **y - | E225 + | ^^ E225 69 | #: E225 70 | z = (x + 1) **y | -E22.py:68:15: E225 Missing whitespace around operator +E22.py:68:13: E225 Missing whitespace around operator | 68 | z = x **y 69 | #: E225 70 | z = (x + 1) **y - | E225 + | ^^ E225 71 | #: E225 72 | z = (x + 1)** y | @@ -85,17 +85,17 @@ E22.py:70:12: E225 Missing whitespace around operator 70 | z = (x + 1) **y 71 | #: E225 72 | z = (x + 1)** y - | E225 + | ^^ E225 73 | #: E225 74 | _1kB = _1MB >>10 | -E22.py:72:15: E225 Missing whitespace around operator +E22.py:72:13: E225 Missing whitespace around operator | 72 | z = (x + 1)** y 73 | #: E225 74 | _1kB = _1MB >>10 - | E225 + | ^^ E225 75 | #: E225 76 | _1kB = _1MB>> 10 | @@ -105,7 +105,7 @@ E22.py:74:12: E225 Missing whitespace around operator 74 | _1kB = _1MB >>10 75 | #: E225 76 | _1kB = _1MB>> 10 - | E225 + | ^^ E225 77 | #: E225 E225 78 | i=i+ 1 | @@ -115,7 +115,7 @@ E22.py:76:2: E225 Missing whitespace around operator 76 | _1kB = _1MB>> 10 77 | #: E225 E225 78 | i=i+ 1 - | E225 + | ^ E225 79 | #: E225 E225 80 | i=i +1 | @@ -125,7 +125,7 @@ E22.py:76:4: E225 Missing whitespace around operator 76 | _1kB = _1MB>> 10 77 | #: E225 E225 78 | i=i+ 1 - | E225 + | ^ E225 79 | #: E225 E225 80 | i=i +1 | @@ -135,17 +135,17 @@ E22.py:78:2: E225 Missing whitespace around operator 78 | i=i+ 1 79 | #: E225 E225 80 | i=i +1 - | E225 + | ^ E225 81 | #: E225 82 | i = 1and 1 | -E22.py:78:6: E225 Missing whitespace around operator +E22.py:78:5: E225 Missing whitespace around operator | 78 | i=i+ 1 79 | #: E225 E225 80 | i=i +1 - | E225 + | ^ E225 81 | #: E225 82 | i = 1and 1 | @@ -155,7 +155,7 @@ E22.py:80:6: E225 Missing whitespace around operator 80 | i=i +1 81 | #: E225 82 | i = 1and 1 - | E225 + | ^^^ E225 83 | #: E225 84 | i = 1or 0 | @@ -165,7 +165,7 @@ E22.py:82:6: E225 Missing whitespace around operator 82 | i = 1and 1 83 | #: E225 84 | i = 1or 0 - | E225 + | ^^ E225 85 | #: E225 86 | 1is 1 | @@ -175,7 +175,7 @@ E22.py:84:2: E225 Missing whitespace around operator 84 | i = 1or 0 85 | #: E225 86 | 1is 1 - | E225 + | ^^ E225 87 | #: E225 88 | 1in [] | @@ -185,17 +185,17 @@ E22.py:86:2: E225 Missing whitespace around operator 86 | 1is 1 87 | #: E225 88 | 1in [] - | E225 + | ^^ E225 89 | #: E225 90 | i = 1 @2 | -E22.py:88:8: E225 Missing whitespace around operator +E22.py:88:7: E225 Missing whitespace around operator | 88 | 1in [] 89 | #: E225 90 | i = 1 @2 - | E225 + | ^ E225 91 | #: E225 92 | i = 1@ 2 | @@ -205,7 +205,7 @@ E22.py:90:6: E225 Missing whitespace around operator 90 | i = 1 @2 91 | #: E225 92 | i = 1@ 2 - | E225 + | ^ E225 93 | #: E225 E226 94 | i=i+1 | @@ -215,17 +215,17 @@ E22.py:92:2: E225 Missing whitespace around operator 92 | i = 1@ 2 93 | #: E225 E226 94 | i=i+1 - | E225 + | ^ E225 95 | #: E225 E226 96 | i =i+1 | -E22.py:94:4: E225 Missing whitespace around operator +E22.py:94:3: E225 Missing whitespace around operator | 94 | i=i+1 95 | #: E225 E226 96 | i =i+1 - | E225 + | ^ E225 97 | #: E225 E226 98 | i= i+1 | @@ -235,17 +235,17 @@ E22.py:96:2: E225 Missing whitespace around operator 96 | i =i+1 97 | #: E225 E226 98 | i= i+1 - | E225 + | ^ E225 99 | #: E225 E226 100 | c = (a +b)*(a - b) | -E22.py:98:9: E225 Missing whitespace around operator +E22.py:98:8: E225 Missing whitespace around operator | 98 | i= i+1 99 | #: E225 E226 100 | c = (a +b)*(a - b) - | E225 + | ^ E225 101 | #: E225 E226 102 | c = (a+ b)*(a - b) | @@ -255,16 +255,16 @@ E22.py:100:7: E225 Missing whitespace around operator 100 | c = (a +b)*(a - b) 101 | #: E225 E226 102 | c = (a+ b)*(a - b) - | E225 + | ^ E225 103 | #: | -E22.py:155:9: E225 Missing whitespace around operator +E22.py:155:7: E225 Missing whitespace around operator | 155 | if not -5 < x < +5: 156 | print >> sys.stderr, "x is out of range." 157 | print >>sys.stdout, "x is an integer." - | E225 + | ^^ E225 158 | x = x / 2 - 1 159 | x = 1 @ 2 | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap index 769264c53d61a..4a9c4abc9c112 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E226_E22.py.snap @@ -6,7 +6,7 @@ E22.py:92:4: E226 Missing whitespace around arithmetic operator 92 | i = 1@ 2 93 | #: E225 E226 94 | i=i+1 - | E226 + | ^ E226 95 | #: E225 E226 96 | i =i+1 | @@ -16,7 +16,7 @@ E22.py:94:5: E226 Missing whitespace around arithmetic operator 94 | i=i+1 95 | #: E225 E226 96 | i =i+1 - | E226 + | ^ E226 97 | #: E225 E226 98 | i= i+1 | @@ -26,7 +26,7 @@ E22.py:96:5: E226 Missing whitespace around arithmetic operator 96 | i =i+1 97 | #: E225 E226 98 | i= i+1 - | E226 + | ^ E226 99 | #: E225 E226 100 | c = (a +b)*(a - b) | @@ -36,7 +36,7 @@ E22.py:98:11: E226 Missing whitespace around arithmetic operator 98 | i= i+1 99 | #: E225 E226 100 | c = (a +b)*(a - b) - | E226 + | ^ E226 101 | #: E225 E226 102 | c = (a+ b)*(a - b) | @@ -46,7 +46,7 @@ E22.py:100:11: E226 Missing whitespace around arithmetic operator 100 | c = (a +b)*(a - b) 101 | #: E225 E226 102 | c = (a+ b)*(a - b) - | E226 + | ^ E226 103 | #: | @@ -54,7 +54,7 @@ E22.py:104:6: E226 Missing whitespace around arithmetic operator | 104 | #: E226 105 | z = 2//30 - | E226 + | ^^ E226 106 | #: E226 E226 107 | c = (a+b) * (a-b) | @@ -64,7 +64,7 @@ E22.py:106:7: E226 Missing whitespace around arithmetic operator 106 | z = 2//30 107 | #: E226 E226 108 | c = (a+b) * (a-b) - | E226 + | ^ E226 109 | #: E226 110 | norman = True+False | @@ -74,7 +74,7 @@ E22.py:106:15: E226 Missing whitespace around arithmetic operator 106 | z = 2//30 107 | #: E226 E226 108 | c = (a+b) * (a-b) - | E226 + | ^ E226 109 | #: E226 110 | norman = True+False | @@ -84,7 +84,7 @@ E22.py:110:6: E226 Missing whitespace around arithmetic operator 110 | norman = True+False 111 | #: E226 112 | x = x*2 - 1 - | E226 + | ^ E226 113 | #: E226 114 | x = x/2 - 1 | @@ -94,7 +94,7 @@ E22.py:112:6: E226 Missing whitespace around arithmetic operator 112 | x = x*2 - 1 113 | #: E226 114 | x = x/2 - 1 - | E226 + | ^ E226 115 | #: E226 E226 116 | hypot2 = x*x + y*y | @@ -104,7 +104,7 @@ E22.py:114:11: E226 Missing whitespace around arithmetic operator 114 | x = x/2 - 1 115 | #: E226 E226 116 | hypot2 = x*x + y*y - | E226 + | ^ E226 117 | #: E226 118 | c = (a + b)*(a - b) | @@ -114,7 +114,7 @@ E22.py:114:17: E226 Missing whitespace around arithmetic operator 114 | x = x/2 - 1 115 | #: E226 E226 116 | hypot2 = x*x + y*y - | E226 + | ^ E226 117 | #: E226 118 | c = (a + b)*(a - b) | @@ -124,7 +124,7 @@ E22.py:116:12: E226 Missing whitespace around arithmetic operator 116 | hypot2 = x*x + y*y 117 | #: E226 118 | c = (a + b)*(a - b) - | E226 + | ^ E226 119 | #: E226 120 | def halves(n): | @@ -134,7 +134,7 @@ E22.py:119:14: E226 Missing whitespace around arithmetic operator 119 | #: E226 120 | def halves(n): 121 | return (i//2 for i in range(n)) - | E226 + | ^^ E226 122 | #: E227 123 | _1kB = _1MB>>10 | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E227_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E227_E22.py.snap index f482ceb131feb..06b73ec109343 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E227_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E227_E22.py.snap @@ -6,7 +6,7 @@ E22.py:121:12: E227 Missing whitespace around bitwise or shift operator 121 | return (i//2 for i in range(n)) 122 | #: E227 123 | _1kB = _1MB>>10 - | E227 + | ^^ E227 124 | #: E227 125 | _1MB = _1kB<<10 | @@ -16,7 +16,7 @@ E22.py:123:12: E227 Missing whitespace around bitwise or shift operator 123 | _1kB = _1MB>>10 124 | #: E227 125 | _1MB = _1kB<<10 - | E227 + | ^^ E227 126 | #: E227 127 | a = b|c | @@ -26,7 +26,7 @@ E22.py:125:6: E227 Missing whitespace around bitwise or shift operator 125 | _1MB = _1kB<<10 126 | #: E227 127 | a = b|c - | E227 + | ^ E227 128 | #: E227 129 | b = c&a | @@ -36,7 +36,7 @@ E22.py:127:6: E227 Missing whitespace around bitwise or shift operator 127 | a = b|c 128 | #: E227 129 | b = c&a - | E227 + | ^ E227 130 | #: E227 131 | c = b^a | @@ -46,7 +46,7 @@ E22.py:129:6: E227 Missing whitespace around bitwise or shift operator 129 | b = c&a 130 | #: E227 131 | c = b^a - | E227 + | ^ E227 132 | #: E228 133 | a = b%c | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E228_E22.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E228_E22.py.snap index fbb2c5c510df9..6c4e0ca52562f 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E228_E22.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E228_E22.py.snap @@ -6,7 +6,7 @@ E22.py:131:6: E228 Missing whitespace around modulo operator 131 | c = b^a 132 | #: E228 133 | a = b%c - | E228 + | ^ E228 134 | #: E228 135 | msg = fmt%(errno, errmsg) | @@ -16,7 +16,7 @@ E22.py:133:10: E228 Missing whitespace around modulo operator 133 | a = b%c 134 | #: E228 135 | msg = fmt%(errno, errmsg) - | E228 + | ^ E228 136 | #: E228 137 | msg = "Error %d occurred"%errno | @@ -26,7 +26,7 @@ E22.py:135:26: E228 Missing whitespace around modulo operator 135 | msg = fmt%(errno, errmsg) 136 | #: E228 137 | msg = "Error %d occurred"%errno - | E228 + | ^ E228 138 | #: | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap index 26828dc7766e3..2560912f77bba 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap @@ -5,7 +5,7 @@ E23.py:2:7: E231 [*] Missing whitespace after ',' | 2 | #: E231 3 | a = (1,2) - | E231 + | ^ E231 4 | #: E231 5 | a[b1,:] | @@ -24,7 +24,7 @@ E23.py:4:5: E231 [*] Missing whitespace after ',' 4 | a = (1,2) 5 | #: E231 6 | a[b1,:] - | E231 + | ^ E231 7 | #: E231 8 | a = [{'a':''}] | @@ -45,7 +45,7 @@ E23.py:6:10: E231 [*] Missing whitespace after ':' 6 | a[b1,:] 7 | #: E231 8 | a = [{'a':''}] - | E231 + | ^ E231 9 | #: Okay 10 | a = (4,) | @@ -66,7 +66,7 @@ E23.py:19:10: E231 [*] Missing whitespace after ',' 19 | def foo() -> None: 20 | #: E231 21 | if (1,2): - | E231 + | ^ E231 22 | pass | = help: Added missing whitespace after ',' diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E251_E25.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E251_E25.py.snap index 723f89470801d..be800bd0fbfa0 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E251_E25.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E251_E25.py.snap @@ -5,7 +5,7 @@ E25.py:2:12: E251 Unexpected spaces around keyword / parameter equals | 2 | #: E251 E251 3 | def foo(bar = False): - | E251 + | ^ E251 4 | '''Test function with an error in declaration''' 5 | pass | @@ -14,7 +14,7 @@ E25.py:2:14: E251 Unexpected spaces around keyword / parameter equals | 2 | #: E251 E251 3 | def foo(bar = False): - | E251 + | ^ E251 4 | '''Test function with an error in declaration''' 5 | pass | @@ -24,7 +24,7 @@ E25.py:6:9: E251 Unexpected spaces around keyword / parameter equals 6 | pass 7 | #: E251 8 | foo(bar= True) - | E251 + | ^ E251 9 | #: E251 10 | foo(bar =True) | @@ -34,7 +34,7 @@ E25.py:8:8: E251 Unexpected spaces around keyword / parameter equals 8 | foo(bar= True) 9 | #: E251 10 | foo(bar =True) - | E251 + | ^ E251 11 | #: E251 E251 12 | foo(bar = True) | @@ -44,7 +44,7 @@ E25.py:10:8: E251 Unexpected spaces around keyword / parameter equals 10 | foo(bar =True) 11 | #: E251 E251 12 | foo(bar = True) - | E251 + | ^ E251 13 | #: E251 14 | y = bar(root= "sdasd") | @@ -54,7 +54,7 @@ E25.py:10:10: E251 Unexpected spaces around keyword / parameter equals 10 | foo(bar =True) 11 | #: E251 E251 12 | foo(bar = True) - | E251 + | ^ E251 13 | #: E251 14 | y = bar(root= "sdasd") | @@ -64,29 +64,33 @@ E25.py:12:14: E251 Unexpected spaces around keyword / parameter equals 12 | foo(bar = True) 13 | #: E251 14 | y = bar(root= "sdasd") - | E251 + | ^ E251 15 | #: E251:2:29 16 | parser.add_argument('--long-option', | E25.py:15:29: E251 Unexpected spaces around keyword / parameter equals | -15 | #: E251:2:29 -16 | parser.add_argument('--long-option', -17 | default= - | E251 -18 | "/rather/long/filesystem/path/here/blah/blah/blah") -19 | #: E251:1:45 +15 | #: E251:2:29 +16 | parser.add_argument('--long-option', +17 | default= + | _____________________________^ +18 | | "/rather/long/filesystem/path/here/blah/blah/blah") + | |____________________^ E251 +19 | #: E251:1:45 +20 | parser.add_argument('--long-option', default | E25.py:18:45: E251 Unexpected spaces around keyword / parameter equals | -18 | "/rather/long/filesystem/path/here/blah/blah/blah") -19 | #: E251:1:45 -20 | parser.add_argument('--long-option', default - | E251 -21 | ="/rather/long/filesystem/path/here/blah/blah/blah") -22 | #: E251:3:8 E251:3:10 +18 | "/rather/long/filesystem/path/here/blah/blah/blah") +19 | #: E251:1:45 +20 | parser.add_argument('--long-option', default + | _____________________________________________^ +21 | | ="/rather/long/filesystem/path/here/blah/blah/blah") + | |____________________^ E251 +22 | #: E251:3:8 E251:3:10 +23 | foo(True, | E25.py:23:8: E251 Unexpected spaces around keyword / parameter equals @@ -94,7 +98,7 @@ E25.py:23:8: E251 Unexpected spaces around keyword / parameter equals 23 | foo(True, 24 | baz=(1, 2), 25 | biz = 'foo' - | E251 + | ^ E251 26 | ) 27 | #: Okay | @@ -104,7 +108,7 @@ E25.py:23:10: E251 Unexpected spaces around keyword / parameter equals 23 | foo(True, 24 | baz=(1, 2), 25 | biz = 'foo' - | E251 + | ^ E251 26 | ) 27 | #: Okay | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E252_E25.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E252_E25.py.snap index 00acfa92e65ea..e6b060c884be5 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E252_E25.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E252_E25.py.snap @@ -6,27 +6,27 @@ E25.py:46:15: E252 Missing whitespace around parameter equals 46 | return a + b 47 | #: E252:1:15 E252:1:16 E252:1:27 E252:1:36 48 | def add(a: int=0, b: int =0, c: int= 0) -> int: - | E252 + | ^ E252 49 | return a + b + c 50 | #: Okay | -E25.py:46:16: E252 Missing whitespace around parameter equals +E25.py:46:15: E252 Missing whitespace around parameter equals | 46 | return a + b 47 | #: E252:1:15 E252:1:16 E252:1:27 E252:1:36 48 | def add(a: int=0, b: int =0, c: int= 0) -> int: - | E252 + | ^ E252 49 | return a + b + c 50 | #: Okay | -E25.py:46:27: E252 Missing whitespace around parameter equals +E25.py:46:26: E252 Missing whitespace around parameter equals | 46 | return a + b 47 | #: E252:1:15 E252:1:16 E252:1:27 E252:1:36 48 | def add(a: int=0, b: int =0, c: int= 0) -> int: - | E252 + | ^ E252 49 | return a + b + c 50 | #: Okay | @@ -36,7 +36,7 @@ E25.py:46:36: E252 Missing whitespace around parameter equals 46 | return a + b 47 | #: E252:1:15 E252:1:16 E252:1:27 E252:1:36 48 | def add(a: int=0, b: int =0, c: int= 0) -> int: - | E252 + | ^ E252 49 | return a + b + c 50 | #: Okay | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap index af6e1e52f0b06..1475638b80e94 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E271_E27.py.snap @@ -6,7 +6,7 @@ E27.py:4:9: E271 Multiple spaces after keyword 4 | True and False 5 | #: E271 6 | True and False - | E271 + | ^^ E271 7 | #: E272 8 | True and False | @@ -16,7 +16,7 @@ E27.py:6:5: E271 Multiple spaces after keyword 6 | True and False 7 | #: E272 8 | True and False - | E271 + | ^^ E271 9 | #: E271 10 | if 1: | @@ -26,7 +26,7 @@ E27.py:8:3: E271 Multiple spaces after keyword 8 | True and False 9 | #: E271 10 | if 1: - | E271 + | ^^^ E271 11 | #: E273 12 | True and False | @@ -36,7 +36,7 @@ E27.py:14:6: E271 Multiple spaces after keyword 14 | True and False 15 | #: E271 16 | a and b - | E271 + | ^^ E271 17 | #: E271 18 | 1 and b | @@ -46,7 +46,7 @@ E27.py:16:6: E271 Multiple spaces after keyword 16 | a and b 17 | #: E271 18 | 1 and b - | E271 + | ^^ E271 19 | #: E271 20 | a and 2 | @@ -56,7 +56,7 @@ E27.py:18:6: E271 Multiple spaces after keyword 18 | 1 and b 19 | #: E271 20 | a and 2 - | E271 + | ^^ E271 21 | #: E271 E272 22 | 1 and b | @@ -66,7 +66,7 @@ E27.py:20:7: E271 Multiple spaces after keyword 20 | a and 2 21 | #: E271 E272 22 | 1 and b - | E271 + | ^^ E271 23 | #: E271 E272 24 | a and 2 | @@ -76,7 +76,7 @@ E27.py:22:7: E271 Multiple spaces after keyword 22 | 1 and b 23 | #: E271 E272 24 | a and 2 - | E271 + | ^^ E271 25 | #: E272 26 | this and False | @@ -86,7 +86,7 @@ E27.py:35:14: E271 Multiple spaces after keyword 35 | from v import c, d 36 | #: E271 37 | from w import (e, f) - | E271 + | ^^ E271 38 | #: E275 39 | from w import(e, f) | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap index 1bc20e60e5be3..eb6a7a5e13075 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E272_E27.py.snap @@ -6,7 +6,7 @@ E27.py:20:2: E272 Multiple spaces before keyword 20 | a and 2 21 | #: E271 E272 22 | 1 and b - | E272 + | ^^ E272 23 | #: E271 E272 24 | a and 2 | @@ -16,7 +16,7 @@ E27.py:22:2: E272 Multiple spaces before keyword 22 | 1 and b 23 | #: E271 E272 24 | a and 2 - | E272 + | ^^ E272 25 | #: E272 26 | this and False | @@ -26,7 +26,7 @@ E27.py:24:5: E272 Multiple spaces before keyword 24 | a and 2 25 | #: E272 26 | this and False - | E272 + | ^^ E272 27 | #: E273 28 | a and b | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap index 87dce5836355a..35cb722185caf 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E273_E27.py.snap @@ -6,7 +6,7 @@ E27.py:10:9: E273 Tab after keyword 10 | if 1: 11 | #: E273 12 | True and False - | E273 + | ^^^^ E273 13 | #: E273 E274 14 | True and False | @@ -16,7 +16,7 @@ E27.py:12:5: E273 Tab after keyword 12 | True and False 13 | #: E273 E274 14 | True and False - | E273 + | ^^^^ E273 15 | #: E271 16 | a and b | @@ -26,7 +26,7 @@ E27.py:12:10: E273 Tab after keyword 12 | True and False 13 | #: E273 E274 14 | True and False - | E273 + | ^^^^ E273 15 | #: E271 16 | a and b | @@ -36,7 +36,7 @@ E27.py:26:6: E273 Tab after keyword 26 | this and False 27 | #: E273 28 | a and b - | E273 + | ^^^^ E273 29 | #: E274 30 | a and b | @@ -46,7 +46,7 @@ E27.py:30:10: E273 Tab after keyword 30 | a and b 31 | #: E273 E274 32 | this and False - | E273 + | ^^^^ E273 33 | #: Okay 34 | from u import (a, b) | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap index 1150a45c696c7..e276c75127498 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E274_E27.py.snap @@ -6,7 +6,7 @@ E27.py:28:3: E274 Tab before keyword 28 | a and b 29 | #: E274 30 | a and b - | E274 + | ^^^^ E274 31 | #: E273 E274 32 | this and False | @@ -16,7 +16,7 @@ E27.py:30:6: E274 Tab before keyword 30 | a and b 31 | #: E273 E274 32 | this and False - | E274 + | ^^^^ E274 33 | #: Okay 34 | from u import (a, b) | diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E275_E27.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E275_E27.py.snap index fa43f95e738da..24f27ec0e4609 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E275_E27.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E275_E27.py.snap @@ -1,52 +1,52 @@ --- source: crates/ruff/src/rules/pycodestyle/mod.rs --- -E27.py:37:14: E275 Missing whitespace after keyword +E27.py:37:8: E275 Missing whitespace after keyword | 37 | from w import (e, f) 38 | #: E275 39 | from w import(e, f) - | E275 + | ^^^^^^ E275 40 | #: E275 41 | from importable.module import(e, f) | -E27.py:39:30: E275 Missing whitespace after keyword +E27.py:39:24: E275 Missing whitespace after keyword | 39 | from w import(e, f) 40 | #: E275 41 | from importable.module import(e, f) - | E275 + | ^^^^^^ E275 42 | #: E275 43 | try: | -E27.py:42:34: E275 Missing whitespace after keyword +E27.py:42:28: E275 Missing whitespace after keyword | 42 | #: E275 43 | try: 44 | from importable.module import(e, f) - | E275 + | ^^^^^^ E275 45 | except ImportError: 46 | pass | -E27.py:46:3: E275 Missing whitespace after keyword +E27.py:46:1: E275 Missing whitespace after keyword | 46 | pass 47 | #: E275 48 | if(foo): - | E275 + | ^^ E275 49 | pass 50 | else: | -E27.py:54:11: E275 Missing whitespace after keyword +E27.py:54:5: E275 Missing whitespace after keyword | 54 | #: E275:2:11 55 | if True: 56 | assert(1) - | E275 + | ^^^^^^ E275 57 | #: Okay 58 | def f(): |