diff --git a/README.rst b/README.rst index 99d60c6..a889cee 100644 --- a/README.rst +++ b/README.rst @@ -184,6 +184,8 @@ It is therefore recommended to use a stacklevel of 2 or greater to provide more **B031**: Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage. Save the result to a list if the result is needed multiple times. +**B032**: Possible unintentional type annotation (using ``:``). Did you mean to assign (using ``=``)? + Opinionated warnings ~~~~~~~~~~~~~~~~~~~~ @@ -333,6 +335,7 @@ Future * B016: Warn when raising f-strings. * Add B028: Check for an explicit stacklevel keyword argument on the warn method from the warnings module. * Add B029: Check when trying to use ``except`` with an empty tuple i.e. ``except: ()``. +* Add B032: Check for possible unintentional type annotations instead of assignments. 23.1.20 ~~~~~~~~~ diff --git a/bugbear.py b/bugbear.py index 503f0cc..b0121ae 100644 --- a/bugbear.py +++ b/bugbear.py @@ -482,6 +482,10 @@ def visit_JoinedStr(self, node): self.check_for_b907(node) self.generic_visit(node) + def visit_AnnAssign(self, node): + self.check_for_b032(node) + self.generic_visit(node) + def check_for_b005(self, node): if node.func.attr not in B005.methods: return # method name doesn't match @@ -1235,6 +1239,21 @@ def check_for_b028(self, node): ): self.errors.append(B028(node.lineno, node.col_offset)) + def check_for_b032(self, node): + if ( + node.value is None + and hasattr(node.target, "value") + and isinstance(node.target.value, ast.Name) + and ( + isinstance(node.target, ast.Subscript) + or ( + isinstance(node.target, ast.Attribute) + and node.target.value.id != "self" + ) + ) + ): + self.errors.append(B032(node.lineno, node.col_offset)) + def compose_call_path(node): if isinstance(node, ast.Attribute): @@ -1625,6 +1644,13 @@ def visit_Lambda(self, node): ) ) +B032 = Error( + message=( + "B032 Possible unintentional type annotation (using `:`). Did you mean to" + " assign (using `=`)?" + ) +) + # Warnings disabled by default. B901 = Error( message=( diff --git a/tests/b032.py b/tests/b032.py new file mode 100644 index 0000000..fc1fa12 --- /dev/null +++ b/tests/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 diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 731f008..fb990ac 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -43,6 +43,7 @@ B029, B030, B031, + B032, B901, B902, B903, @@ -471,6 +472,22 @@ def test_b031(self): ) self.assertEqual(errors, expected) + def test_b032(self): + filename = Path(__file__).absolute().parent / "b032.py" + bbc = BugBearChecker(filename=str(filename)) + errors = list(bbc.run()) + expected = self.errors( + B032(9, 0), + B032(10, 0), + B032(12, 0), + B032(13, 0), + B032(16, 0), + B032(17, 0), + B032(18, 0), + B032(19, 0), + ) + self.assertEqual(errors, expected) + @unittest.skipIf(sys.version_info < (3, 8), "not implemented for <3.8") def test_b907(self): filename = Path(__file__).absolute().parent / "b907.py"