Repository: MartinThoma/flake8-simplify (python)

Issue:
[New Rule] Create dictionary directly
## Explanation

Do not use a line to create a dictionary, and then populate it. Do it directly. PyCharm has this check, which is rather neat; I think it's in Pylint as I think this linter is used behind the scenes when you run code inspection on a project. It's a bit faster (nanoseconds, whatever), and it reads nicer.

## Example

```python
# Bad
a = {}
a['b] = 'c'

# Good
a = {'b': 'c'}
```


Pull request:
SIM124 (SIM904): Check for dictionary pre-initialization
* SIM904

Closes #92

Patch:
diff --git a/README.md b/README.md
index b833707..c7828e6 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ Python-specific rules:
 * [`SIM117`](https://github.com/MartinThoma/flake8-simplify/issues/35): Merge with-statements that use the same scope ([example](#SIM117))
 * [`SIM119`](https://github.com/MartinThoma/flake8-simplify/issues/37) ![](https://shields.io/badge/-legacyfix-inactive): Use dataclasses for data containers ([example](#SIM119))
 * `SIM120` ![](https://shields.io/badge/-legacyfix-inactive): Use 'class FooBar:' instead of 'class FooBar(object):' ([example](#SIM120))
+* `SIM124`: Reserved for SIM904 once it's stable
 
 Simplifying Comparations:
 
@@ -92,6 +93,7 @@ the code will change to another number.
 Current experimental rules:
 
 * `SIM901`: Use comparisons directly instead of wrapping them in a `bool(...)` call ([example](#SIM901))
+* `SIM904`: Assign values to dictionary directly at initialization ([example](#SIM904))
 
 ## Disabling Rules
 
@@ -545,3 +547,14 @@ bool(a == b)
 # Good
 a == b
 ```
+
+### SIM904
+
+```python
+# Bad
+a = {}
+a["b"] = "c"
+
+# Good
+a = {"b": "c"}
+```
diff --git a/flake8_simplify.py b/flake8_simplify.py
index 88eb2a8..f91318a 100644
--- a/flake8_simplify.py
+++ b/flake8_simplify.py
@@ -108,6 +108,7 @@ SIM401 = (
     "instead of an if-block"
 )
 SIM901 = "SIM901 Use '{better}' instead of '{current}'"
+SIM904 = "SIM904 Initialize dictionary '{dict_name}' directly"
 
 # ast.Constant in Python 3.8, ast.NameConstant in Python 3.6 and 3.7
 BOOL_CONST_TYPES = (ast.Constant, ast.NameConstant)
@@ -1838,10 +1839,71 @@ def _get_sim901(node: ast.Call) -> List[Tuple[int, int, str]]:
     return errors
 
 
+def _get_sim904(node: ast.Assign) -> List[Tuple[int, int, str]]:
+    """
+    Assign values to dictionary directly at initialization.
+
+    Example
+    -------
+    Code:
+        # Bad
+        a = { }
+        a['b] = 'c'
+
+        # Good
+        a = {'b': 'c'}
+    Bad AST:
+        [
+            Assign(
+                targets=[Name(id='a', ctx=Store())],
+                value=Dict(keys=[], values=[]),
+                type_comment=None,
+            ),
+            Assign(
+                targets=[
+                    Subscript(
+                        value=Name(id='a', ctx=Load()),
+                        slice=Constant(value='b', kind=None),
+                        ctx=Store(),
+                    ),
+                ],
+                value=Constant(value='c', kind=None),
+                type_comment=None,
+            ),
+        ]
+    """
+    errors: List[Tuple[int, int, str]] = []
+    n2 = node.next_sibling  # type: ignore
+    if not (
+        isinstance(node.value, ast.Dict)
+        and isinstance(n2, ast.Assign)
+        and len(n2.targets) == 1
+        and len(node.targets) == 1
+        and isinstance(n2.targets[0], ast.Subscript)
+        and isinstance(n2.targets[0].value, ast.Name)
+        and isinstance(node.targets[0], ast.Name)
+        and n2.targets[0].value.id == node.targets[0].id
+    ):
+        return errors
+    dict_name = to_source(node.targets[0])
+    errors.append(
+        (
+            node.lineno,
+            node.col_offset,
+            SIM904.format(dict_name=dict_name),
+        )
+    )
+    return errors
+
+
 class Visitor(ast.NodeVisitor):
     def __init__(self) -> None:
         self.errors: List[Tuple[int, int, str]] = []
 
+    def visit_Assign(self, node: ast.Assign) -> Any:
+        self.errors += _get_sim904(node)
+        self.generic_visit(node)
+
     def visit_Call(self, node: ast.Call) -> Any:
         self.errors += _get_sim115(Call(node))
         self.errors += _get_sim901(node)


Test patch:
diff --git a/tests/test_simplify.py b/tests/test_simplify.py
index 5443746..0304702 100644
--- a/tests/test_simplify.py
+++ b/tests/test_simplify.py
@@ -935,3 +935,11 @@ else:
 def test_sim901():
     results = _results("bool(a == b)")
     assert results == {"1:0 SIM901 Use 'a == b' instead of 'bool(a == b)'"}
+
+
+def test_sim904():
+    results = _results(
+        """a = { }
+a['b'] = 'c'"""
+    )
+    assert results == {"1:0 SIM904 Initialize dictionary 'a' directly"}