diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B032.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B032.py new file mode 100644 index 0000000000000..85044ae3a8536 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B032.py @@ -0,0 +1,29 @@ +""" +Should emit: +B032 - on lines 9, 10, 12, 13, 16-19 +""" + +# Flag these +dct = {"a": 1} + +dct["b"]: 2 +dct.b: 2 + +dct["b"]: "test" +dct.b: "test" + +test = "test" +dct["b"]: test +dct["b"]: test.lower() +dct.b: test +dct.b: test.lower() + +# Do not flag below +typed_dct: dict[str, int] = {"a": 1} +typed_dct["b"] = 2 +typed_dct.b = 2 + + +class TestClass: + def test_self(self): + self.test: int \ No newline at end of file diff --git a/crates/ruff/src/checkers/ast.rs b/crates/ruff/src/checkers/ast.rs index be89f94ecf7e6..482cd2960fc04 100644 --- a/crates/ruff/src/checkers/ast.rs +++ b/crates/ruff/src/checkers/ast.rs @@ -1842,6 +1842,18 @@ where pycodestyle::rules::lambda_assignment(self, target, value, stmt); } } + if self + .settings + .rules + .enabled(&Rule::UnintentionalTypeAnnotation) + { + flake8_bugbear::rules::unintentional_type_annotation( + self, + target, + value.as_deref(), + stmt, + ); + } } StmtKind::Delete { .. } => {} StmtKind::Expr { value, .. } => { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index e80b4accdcd73..2fe096ed22e42 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -180,6 +180,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Bugbear, "026") => Rule::StarArgUnpackingAfterKeywordArg, (Flake8Bugbear, "027") => Rule::EmptyMethodWithoutAbstractDecorator, (Flake8Bugbear, "029") => Rule::ExceptWithEmptyTuple, + (Flake8Bugbear, "032") => Rule::UnintentionalTypeAnnotation, (Flake8Bugbear, "904") => Rule::RaiseWithoutFromInsideExcept, (Flake8Bugbear, "905") => Rule::ZipWithoutExplicitStrict, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 74c1bf65744eb..419994f23bc3c 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -183,6 +183,7 @@ ruff_macros::register_rules!( rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict, rules::flake8_bugbear::rules::ExceptWithEmptyTuple, + rules::flake8_bugbear::rules::UnintentionalTypeAnnotation, // flake8-blind-except rules::flake8_blind_except::rules::BlindExcept, // flake8-comprehensions diff --git a/crates/ruff/src/rules/flake8_bugbear/mod.rs b/crates/ruff/src/rules/flake8_bugbear/mod.rs index c91d9757dc6f8..b9c43ef6c160f 100644 --- a/crates/ruff/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/mod.rs @@ -42,6 +42,7 @@ mod tests { #[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"); "B027")] #[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.pyi"); "B027_pyi")] #[test_case(Rule::ExceptWithEmptyTuple, Path::new("B029.py"); "B029")] + #[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"); "B032")] #[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"); "B904")] #[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"); "B905")] fn rules(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs index 73283b204382b..0e4fac8e61f59 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs @@ -41,6 +41,10 @@ pub use useless_contextlib_suppress::{useless_contextlib_suppress, UselessContex pub use useless_expression::{useless_expression, UselessExpression}; pub use zip_without_explicit_strict::{zip_without_explicit_strict, ZipWithoutExplicitStrict}; +pub use unintentional_type_annotation::{ + unintentional_type_annotation, UnintentionalTypeAnnotation, +}; + mod abstract_base_class; mod assert_false; mod assert_raises_exception; @@ -62,6 +66,7 @@ mod setattr_with_constant; mod star_arg_unpacking_after_keyword_arg; mod strip_with_multi_characters; mod unary_prefix_increment; +mod unintentional_type_annotation; mod unreliable_callable_check; mod unused_loop_control_variable; mod useless_comparison; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs new file mode 100644 index 0000000000000..255f8e5833303 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs @@ -0,0 +1,69 @@ +use rustpython_parser::ast::{Expr, ExprKind, Stmt}; + +use ruff_macros::{define_violation, derive_message_formats}; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::registry::Diagnostic; +use crate::violation::Violation; + +define_violation!( + /// ## What it does + /// Checks for the unintentional use of type annotations. + /// + /// ## Why is this bad? + /// The use of a colon (`:`) in lieu of an assignment (`=`) can be syntactically valid, but + /// is almost certainly a mistake when used in a subscript or attribute assignment. + /// + /// ## Example + /// ```python + /// a["b"]: 1 + /// ``` + /// + /// Use instead: + /// ```python + /// a["b"] = 1 + /// ``` + pub struct UnintentionalTypeAnnotation; +); +impl Violation for UnintentionalTypeAnnotation { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Possible unintentional type annotation (using `:`). Did you mean to assign (using `=`)?" + ) + } +} + +/// B032 +pub fn unintentional_type_annotation( + checker: &mut Checker, + target: &Expr, + value: Option<&Expr>, + stmt: &Stmt, +) { + if value.is_some() { + return; + } + match &target.node { + ExprKind::Subscript { value, .. } => { + if matches!(&value.node, ExprKind::Name { .. }) { + checker.diagnostics.push(Diagnostic::new( + UnintentionalTypeAnnotation, + Range::from_located(stmt), + )); + } + } + ExprKind::Attribute { value, .. } => { + if let ExprKind::Name { id, .. } = &value.node { + if id != "self" { + checker.diagnostics.push(Diagnostic::new( + UnintentionalTypeAnnotation, + Range::from_located(stmt), + )); + } + } + } + _ => {} + }; +} diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B032_B032.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B032_B032.py.snap new file mode 100644 index 0000000000000..558164a92552c --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B032_B032.py.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff/src/rules/flake8_bugbear/mod.rs +expression: diagnostics +--- +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 9 + column: 0 + end_location: + row: 9 + column: 11 + fix: ~ + parent: ~ +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 10 + column: 0 + end_location: + row: 10 + column: 8 + fix: ~ + parent: ~ +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 12 + column: 0 + end_location: + row: 12 + column: 16 + fix: ~ + parent: ~ +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 13 + column: 0 + end_location: + row: 13 + column: 13 + fix: ~ + parent: ~ +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 16 + column: 0 + end_location: + row: 16 + column: 14 + fix: ~ + parent: ~ +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 17 + column: 0 + end_location: + row: 17 + column: 22 + fix: ~ + parent: ~ +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 18 + column: 0 + end_location: + row: 18 + column: 11 + fix: ~ + parent: ~ +- kind: + UnintentionalTypeAnnotation: ~ + location: + row: 19 + column: 0 + end_location: + row: 19 + column: 19 + fix: ~ + parent: ~ + diff --git a/ruff.schema.json b/ruff.schema.json index 710060c7f0285..569f1c16f9b5b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1430,6 +1430,8 @@ "B026", "B027", "B029", + "B03", + "B032", "B9", "B90", "B904",