Repository: MartinThoma/flake8-simplify (python)

Issue:
[New Rule]: rewrite dict.get(key, None) as dict.get(key)
## Explanation

`dict.get(key)` returns `None` by default if `key` is missing.

## Example

```python
# Bad
dict.get(key, None)

# Good
dict.get(key)
```


## Related

Adding this rule should be coordinated with https://github.com/charliermarsh/ruff/issues/3546 for consistent naming.

Pull request description:
ENH: Add `dict.get(key, None)` → `dict.get(key)` as SIM910
close #171 

This PR implements the rules of `SIM910`.
This rule detects `dict.get(key, None)`.

ref:
https://docs.python.org/3/library/stdtypes.html?highlight=dict#dict.get

Patch:
diff --git a/README.md b/README.md
index b5f46f2..c1ef70d 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,7 @@ Current experimental rules:
 * [`SIM907`](https://github.com/MartinThoma/flake8-simplify/issues/64): Use Optional[Type] instead of Union[Type, None] ([example](#SIM907))
 * [`SIM908`](https://github.com/MartinThoma/flake8-simplify/issues/50): Use dict.get(key) ([example](#SIM908))
 * [`SIM909`](https://github.com/MartinThoma/flake8-simplify/issues/114): Avoid reflexive assignments ([example](#SIM909))
+* [`SIM910`](https://github.com/MartinThoma/flake8-simplify/issues/171): Avoid to use `dict.get(key, None)` ([example](#SIM910))
 
 ## Disabling Rules
 
@@ -614,3 +615,13 @@ foo = foo = 42
 # Good
 foo = 42
 ```
+
+### SIM910
+
+```python
+# Bad
+dict.get(key, None)
+
+# Good
+dict.get(key)
+```
diff --git a/flake8_simplify/__init__.py b/flake8_simplify/__init__.py
index 2207de6..3fd1f1e 100644
--- a/flake8_simplify/__init__.py
+++ b/flake8_simplify/__init__.py
@@ -19,6 +19,7 @@ from flake8_simplify.rules.ast_call import (
     get_sim901,
     get_sim905,
     get_sim906,
+    get_sim910,
 )
 from flake8_simplify.rules.ast_classdef import get_sim120
 from flake8_simplify.rules.ast_compare import get_sim118, get_sim300
@@ -74,6 +75,7 @@ class Visitor(ast.NodeVisitor):
         self.errors += get_sim901(node)
         self.errors += get_sim905(node)
         self.errors += get_sim906(node)
+        self.errors += get_sim910(Call(node))
         self.generic_visit(node)
 
     def visit_With(self, node: ast.With) -> Any:
diff --git a/flake8_simplify/rules/ast_call.py b/flake8_simplify/rules/ast_call.py
index 0c2f047..60947f6 100644
--- a/flake8_simplify/rules/ast_call.py
+++ b/flake8_simplify/rules/ast_call.py
@@ -5,6 +5,7 @@ import logging
 from typing import List, Tuple
 
 # First party
+from flake8_simplify.constants import BOOL_CONST_TYPES
 from flake8_simplify.utils import Call, to_source
 
 logger = logging.getLogger(__name__)
@@ -180,3 +181,55 @@ def get_sim906(node: ast.Call) -> List[Tuple[int, int, str]]:
         )
     )
     return errors
+
+
+def get_sim910(node: Call) -> List[Tuple[int, int, str]]:
+    """
+    Get a list of all usages of "dict.get(key, None)"
+
+    Example AST
+    -----------
+        Expr(
+            value=Call(
+                func=Attribute(
+                    value=Name(id='a_dict', ctx=Load()),
+                    attr='get',
+                    ctx=Load()
+                ),
+                args=[
+                    Name(id='key', ctx=Load()),
+                    Constant(value=None)
+                ],
+                keywords=[]
+            ),
+        ),
+    """
+    RULE = "SIM910 Use '{expected}' instead of '{actual}'"
+    errors: List[Tuple[int, int, str]] = []
+    if not (
+        isinstance(node.func, ast.Attribute)
+        and node.func.attr == "get"
+        and isinstance(node.func.ctx, ast.Load)
+    ):
+        return errors
+
+    # check the argument value
+    if not (
+        len(node.args) == 2
+        and isinstance(node.args[1], BOOL_CONST_TYPES)
+        and node.args[1].value is None
+    ):
+        return errors
+
+    actual = to_source(node)
+    func = to_source(node.func)
+    key = to_source(node.args[0])
+    expected = f"{func}({key})"
+    errors.append(
+        (
+            node.lineno,
+            node.col_offset,
+            RULE.format(actual=actual, expected=expected),
+        )
+    )
+    return errors


Test patch:
diff --git a/tests/test_900_rules.py b/tests/test_900_rules.py
index 029080b..3f1d17b 100644
--- a/tests/test_900_rules.py
+++ b/tests/test_900_rules.py
@@ -181,3 +181,26 @@ def test_sim909_false_positives(s):
     results = _results(s)
     for result in results:
         assert "SIM909" not in result
+
+
+@pytest.mark.parametrize(
+    ("s", "msg"),
+    (
+        (
+            "d.get(key, None)",
+            "1:0 SIM910 Use 'd.get(key)' instead of 'd.get(key, None)'",
+        ),
+        (
+            "d.get('key', None)",
+            "1:0 SIM910 Use 'd.get(\"key\")' instead of 'd.get('key', None)'",
+        ),
+        (
+            "d.get(key)",
+            None,
+        ),
+    ),
+)
+def test_sim910(s, msg):
+    expected = {msg} if msg is not None else set()
+    results = _results(s)
+    assert results == expected