Repository: Instagram/Fixit (language: python)

Issue:
no_assert_equals is incomplete
The rule `no_assert_equals` is incomplete; it only checks for `assertEquals`. There are probably 7 cases that the linter could fix, according to the Python 3 docs.

I'm happy to produce a PR for this, but before I do (and have to do it a different way):
* One rule file per deprecated alias, with one match and replace per file?
* One rule file for all deprecated aliases with a loop to match and replace?


Discussion / hints:
I think a single rule/file that handles all deprecated aliases would be better, and result in less duplicate code and fewer visits to the syntax tree.

Fix patch:
diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst
index 05ed9540..b0274556 100644
--- a/docs/guide/builtins.rst
+++ b/docs/guide/builtins.rst
@@ -15,8 +15,8 @@ unless explicitly listed in the :attr:`disable` configuration option.
 - :class:`CollapseIsinstanceChecksRule`
 - :class:`ComparePrimitivesByEqualRule`
 - :class:`CompareSingletonPrimitivesByIsRule`
+- :class:`DeprecatedUnittestAssertsRule`
 - :class:`ExplicitFrozenDataclassRule`
-- :class:`NoAssertEqualsRule`
 - :class:`NoAssertTrueForComparisonsRule`
 - :class:`NoInheritFromObjectRule`
 - :class:`NoNamedTupleRule`
@@ -237,6 +237,45 @@ unless explicitly listed in the :attr:`disable` configuration option.
             # suggested fix
             x is not False
 
+.. class:: DeprecatedUnittestAssertsRule
+
+    Discourages the use of various deprecated unittest.TestCase functions
+
+    See https://docs.python.org/3/library/unittest.html#deprecated-aliases
+    
+
+    .. attribute:: MESSAGE
+
+        {deprecated} is deprecated, use {replacement} instead
+    .. attribute:: AUTOFIX
+        :type: Yes
+
+
+    .. attribute:: VALID
+
+        .. code:: python
+
+            self.assertEqual(a, b)
+        .. code:: python
+
+            self.assertNotEqual(a, b)
+
+    .. attribute:: INVALID
+
+        .. code:: python
+
+            self.assertEquals(a, b)
+
+            # suggested fix
+            self.assertEqual(a, b)
+
+        .. code:: python
+
+            self.assertNotEquals(a, b)
+
+            # suggested fix
+            self.assertNotEqual(a, b)
+
 .. class:: ExplicitFrozenDataclassRule
 
     Encourages the use of frozen dataclass objects by telling users to specify the
@@ -293,35 +332,6 @@ unless explicitly listed in the :attr:`disable` configuration option.
             @dataclass(frozen=True)  # called as a function, no kwargs
             class Cls: pass
 
-.. class:: NoAssertEqualsRule
-
-    Discourages use of ``assertEquals`` as it is deprecated (see https://docs.python.org/2/library/unittest.html#deprecated-aliases
-    and https://bugs.python.org/issue9424). Use the standardized ``assertEqual`` instead.
-    
-
-    .. attribute:: MESSAGE
-
-        "assertEquals" is deprecated, use "assertEqual" instead.
-        See https://docs.python.org/2/library/unittest.html#deprecated-aliases and https://bugs.python.org/issue9424.
-    .. attribute:: AUTOFIX
-        :type: Yes
-
-
-    .. attribute:: VALID
-
-        .. code:: python
-
-            self.assertEqual(a, b)
-
-    .. attribute:: INVALID
-
-        .. code:: python
-
-            self.assertEquals(a, b)
-
-            # suggested fix
-            self.assertEqual(a, b)
-
 .. class:: NoAssertTrueForComparisonsRule
 
     Finds incorrect use of ``assertTrue`` when the intention is to compare two values.
diff --git a/src/fixit/rules/no_assert_equals.py b/src/fixit/rules/no_assert_equals.py
deleted file mode 100644
index c5b9e018..00000000
--- a/src/fixit/rules/no_assert_equals.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright (c) Meta Platforms, Inc. and affiliates.
-#
-# This source code is licensed under the MIT license found in the
-# LICENSE file in the root directory of this source tree.
-
-import libcst as cst
-import libcst.matchers as m
-
-from fixit import CstLintRule, InvalidTestCase as Invalid, ValidTestCase as Valid
-
-
-class NoAssertEqualsRule(CstLintRule):
-    """
-    Discourages use of ``assertEquals`` as it is deprecated (see https://docs.python.org/2/library/unittest.html#deprecated-aliases
-    and https://bugs.python.org/issue9424). Use the standardized ``assertEqual`` instead.
-    """
-
-    MESSAGE: str = (
-        '"assertEquals" is deprecated, use "assertEqual" instead.\n'
-        + "See https://docs.python.org/2/library/unittest.html#deprecated-aliases and https://bugs.python.org/issue9424."
-    )
-    VALID = [Valid("self.assertEqual(a, b)")]
-    INVALID = [
-        Invalid(
-            "self.assertEquals(a, b)",
-            expected_replacement="self.assertEqual(a, b)",
-        )
-    ]
-
-    def visit_Call(self, node: cst.Call) -> None:
-        if m.matches(
-            node,
-            m.Call(func=m.Attribute(value=m.Name("self"), attr=m.Name("assertEquals"))),
-        ):
-            new_call = node.with_deep_changes(
-                old_node=cst.ensure_type(node.func, cst.Attribute).attr,
-                value="assertEqual",
-            )
-            self.report(node, replacement=new_call)


Test patch:
diff --git a/src/fixit/rules/deprecated_unittest_asserts.py b/src/fixit/rules/deprecated_unittest_asserts.py
new file mode 100644
index 00000000..def8afba
--- /dev/null
+++ b/src/fixit/rules/deprecated_unittest_asserts.py
@@ -0,0 +1,95 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+import libcst as cst
+import libcst.matchers as m
+
+from fixit import CstLintRule, InvalidTestCase as Invalid, ValidTestCase as Valid
+
+
+class DeprecatedUnittestAssertsRule(CstLintRule):
+    """
+    Discourages the use of various deprecated unittest.TestCase functions
+
+    See https://docs.python.org/3/library/unittest.html#deprecated-aliases
+    """
+
+    MESSAGE: str = "{deprecated} is deprecated, use {replacement} instead"
+    VALID = [
+        # correct methods
+        Valid("self.assertEqual(a, b)"),
+        Valid("self.assertNotEqual(a, b)"),
+        Valid("self.assertAlmostEqual(a, b)"),
+        Valid("self.assertNotAlmostEqual(a, b)"),
+        Valid("self.assertRegex(text, regex)"),
+        Valid("self.assertNotRegex(text, regex)"),
+        Valid("self.assertRaisesRegex(exception, regex)"),
+        # not test methods
+        Valid("obj.assertEquals(a, b)"),
+        Valid("obj.assertNotEquals(a, b)"),
+    ]
+    INVALID = [
+        Invalid(
+            "self.assertEquals(a, b)",
+            expected_message="assertEquals is deprecated, use assertEqual instead",
+            expected_replacement="self.assertEqual(a, b)",
+        ),
+        Invalid(
+            "self.assertNotEquals(a, b)",
+            expected_message="assertNotEquals is deprecated, use assertNotEqual instead",
+            expected_replacement="self.assertNotEqual(a, b)",
+        ),
+        Invalid(
+            "self.assertAlmostEquals(a, b)",
+            expected_message="assertAlmostEquals is deprecated, use assertAlmostEqual instead",
+            expected_replacement="self.assertAlmostEqual(a, b)",
+        ),
+        Invalid(
+            "self.assertNotAlmostEquals(a, b)",
+            expected_message="assertNotAlmostEquals is deprecated, use assertNotAlmostEqual instead",
+            expected_replacement="self.assertNotAlmostEqual(a, b)",
+        ),
+        Invalid(
+            "self.assertRegexpMatches(text, regex)",
+            expected_message="assertRegexpMatches is deprecated, use assertRegex instead",
+            expected_replacement="self.assertRegex(text, regex)",
+        ),
+        Invalid(
+            "self.assertNotRegexpMatches(text, regex)",
+            expected_message="assertNotRegexpMatches is deprecated, use assertNotRegex instead",
+            expected_replacement="self.assertNotRegex(text, regex)",
+        ),
+        Invalid(
+            "self.assertRaisesRegexp(exception, regex)",
+            expected_message="assertRaisesRegexp is deprecated, use assertRaisesRegex instead",
+            expected_replacement="self.assertRaisesRegex(exception, regex)",
+        ),
+    ]
+
+    def visit_Call(self, node: cst.Call) -> None:
+        replacements = {
+            "assertEquals": "assertEqual",
+            "assertNotEquals": "assertNotEqual",
+            "assertAlmostEquals": "assertAlmostEqual",
+            "assertNotAlmostEquals": "assertNotAlmostEqual",
+            "assertRegexpMatches": "assertRegex",
+            "assertNotRegexpMatches": "assertNotRegex",
+            "assertRaisesRegexp": "assertRaisesRegex",
+        }
+        for deprecated, replacement in replacements.items():
+            if m.matches(
+                node,
+                m.Call(func=m.Attribute(value=m.Name("self"), attr=m.Name(deprecated))),
+            ):
+                new_call = node.with_deep_changes(
+                    old_node=cst.ensure_type(node.func, cst.Attribute).attr,
+                    value=replacement,
+                )
+                self.report(
+                    node,
+                    self.MESSAGE.format(deprecated=deprecated, replacement=replacement),
+                    replacement=new_call,
+                )
+                break