Skip to content

Commit

Permalink
Add a new check dict-init-mutate (#7794)
Browse files Browse the repository at this point in the history
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
  • Loading branch information
clavedeluna and Pierre-Sassoulas committed Nov 23, 2022
1 parent ed404d3 commit f7d681b
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 0 deletions.
3 changes: 3 additions & 0 deletions doc/data/messages/d/dict-init-mutate/bad.py
@@ -0,0 +1,3 @@
fruit_prices = {} # [dict-init-mutate]
fruit_prices['apple'] = 1
fruit_prices['banana'] = 10
1 change: 1 addition & 0 deletions doc/data/messages/d/dict-init-mutate/good.py
@@ -0,0 +1 @@
fruit_prices = {"apple": 1, "banana": 10}
2 changes: 2 additions & 0 deletions doc/data/messages/d/dict-init-mutate/pylintrc
@@ -0,0 +1,2 @@
[MAIN]
load-plugins=pylint.extensions.dict_init_mutate,
16 changes: 16 additions & 0 deletions doc/user_guide/checkers/extensions.rst
Expand Up @@ -14,6 +14,7 @@ Pylint provides the following optional plugins:
- :ref:`pylint.extensions.comparison_placement`
- :ref:`pylint.extensions.confusing_elif`
- :ref:`pylint.extensions.consider_ternary_expression`
- :ref:`pylint.extensions.dict_init_mutate`
- :ref:`pylint.extensions.docparams`
- :ref:`pylint.extensions.docstyle`
- :ref:`pylint.extensions.dunder`
Expand Down Expand Up @@ -264,6 +265,21 @@ Design checker Messages
Cyclomatic


.. _pylint.extensions.dict_init_mutate:

Dict-Init-Mutate checker
~~~~~~~~~~~~~~~~~~~~~~~~

This checker is provided by ``pylint.extensions.dict_init_mutate``.
Verbatim name of the checker is ``dict-init-mutate``.

Dict-Init-Mutate checker Messages
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:dict-init-mutate (C3401): *Dictionary mutated immediately after initialization*
Dictionaries can be initialized with a single statement using dictionary
literal syntax.


.. _pylint.extensions.docstyle:

Docstyle checker
Expand Down
1 change: 1 addition & 0 deletions doc/user_guide/messages/messages_overview.rst
Expand Up @@ -231,6 +231,7 @@ All messages in the warning category:
warning/deprecated-method
warning/deprecated-module
warning/deprecated-typing-alias
warning/dict-init-mutate
warning/differing-param-doc
warning/differing-type-doc
warning/duplicate-except
Expand Down
4 changes: 4 additions & 0 deletions doc/whatsnew/fragments/2876.new_check
@@ -0,0 +1,4 @@
Add new extension checker ``dict-init-mutate`` that flags mutating a dictionary immediately
after the dictionary was created.

Closes #2876
66 changes: 66 additions & 0 deletions pylint/extensions/dict_init_mutate.py
@@ -0,0 +1,66 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

"""Check for use of dictionary mutation after initialization."""
from __future__ import annotations

from typing import TYPE_CHECKING

from astroid import nodes

from pylint.checkers import BaseChecker
from pylint.checkers.utils import only_required_for_messages
from pylint.interfaces import HIGH

if TYPE_CHECKING:
from pylint.lint.pylinter import PyLinter


class DictInitMutateChecker(BaseChecker):
name = "dict-init-mutate"
msgs = {
"C3401": (
"Declare all known key/values when initializing the dictionary.",
"dict-init-mutate",
"Dictionaries can be initialized with a single statement "
"using dictionary literal syntax.",
)
}

@only_required_for_messages("dict-init-mutate")
def visit_assign(self, node: nodes.Assign) -> None:
"""
Detect dictionary mutation immediately after initialization.
At this time, detecting nested mutation is not supported.
"""
if not isinstance(node.value, nodes.Dict):
return

dict_name = node.targets[0]
if len(node.targets) != 1 or not isinstance(dict_name, nodes.AssignName):
return

first_sibling = node.next_sibling()
if (
not first_sibling
or not isinstance(first_sibling, nodes.Assign)
or len(first_sibling.targets) != 1
):
return

sibling_target = first_sibling.targets[0]
if not isinstance(sibling_target, nodes.Subscript):
return

sibling_name = sibling_target.value
if not isinstance(sibling_name, nodes.Name):
return

if sibling_name.name == dict_name.name:
self.add_message("dict-init-mutate", node=node, confidence=HIGH)


def register(linter: PyLinter) -> None:
linter.register_checker(DictInitMutateChecker(linter))
38 changes: 38 additions & 0 deletions tests/functional/ext/dict_init_mutate.py
@@ -0,0 +1,38 @@
"""Example cases for dict-init-mutate"""
# pylint: disable=use-dict-literal, invalid-name

base = {}

fruits = {}
for fruit in ["apple", "orange"]:
fruits[fruit] = 1
fruits[fruit] += 1

count = 10
fruits = {"apple": 1}
fruits["apple"] += count

config = {} # [dict-init-mutate]
config['pwd'] = 'hello'

config = {} # [dict-init-mutate]
config['dir'] = 'bin'
config['user'] = 'me'
config['workers'] = 5
print(config)

config = {} # Not flagging calls to update for now
config.update({"dir": "bin"})

config = {} # [dict-init-mutate]
config['options'] = {} # Identifying nested assignment not supporting this yet.
config['options']['debug'] = False
config['options']['verbose'] = True


config = {}
def update_dict(di):
"""Update a dictionary"""
di["one"] = 1

update_dict(config)
2 changes: 2 additions & 0 deletions tests/functional/ext/dict_init_mutate.rc
@@ -0,0 +1,2 @@
[MAIN]
load-plugins=pylint.extensions.dict_init_mutate,
3 changes: 3 additions & 0 deletions tests/functional/ext/dict_init_mutate.txt
@@ -0,0 +1,3 @@
dict-init-mutate:15:0:15:11::Declare all known key/values when initializing the dictionary.:HIGH
dict-init-mutate:18:0:18:11::Declare all known key/values when initializing the dictionary.:HIGH
dict-init-mutate:27:0:27:11::Declare all known key/values when initializing the dictionary.:HIGH

0 comments on commit f7d681b

Please sign in to comment.