summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDani Alcala <112832187+clavedeluna@users.noreply.github.com>2022-11-23 14:11:20 -0300
committerGitHub <noreply@github.com>2022-11-23 18:11:20 +0100
commitf7d681b5a79e5781ab8072fe64459b199955a1f6 (patch)
tree57e5c051cf7384ae01d50007386b73b1dab616e1
parented404d361f24f068693d59619961e575810af3d9 (diff)
downloadpylint-git-f7d681b5a79e5781ab8072fe64459b199955a1f6.tar.gz
Add a new check `dict-init-mutate` (#7794)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
-rw-r--r--doc/data/messages/d/dict-init-mutate/bad.py3
-rw-r--r--doc/data/messages/d/dict-init-mutate/good.py1
-rw-r--r--doc/data/messages/d/dict-init-mutate/pylintrc2
-rw-r--r--doc/user_guide/checkers/extensions.rst16
-rw-r--r--doc/user_guide/messages/messages_overview.rst1
-rw-r--r--doc/whatsnew/fragments/2876.new_check4
-rw-r--r--pylint/extensions/dict_init_mutate.py66
-rw-r--r--tests/functional/ext/dict_init_mutate.py38
-rw-r--r--tests/functional/ext/dict_init_mutate.rc2
-rw-r--r--tests/functional/ext/dict_init_mutate.txt3
10 files changed, 136 insertions, 0 deletions
diff --git a/doc/data/messages/d/dict-init-mutate/bad.py b/doc/data/messages/d/dict-init-mutate/bad.py
new file mode 100644
index 000000000..d6d1cfe18
--- /dev/null
+++ b/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
diff --git a/doc/data/messages/d/dict-init-mutate/good.py b/doc/data/messages/d/dict-init-mutate/good.py
new file mode 100644
index 000000000..02137f287
--- /dev/null
+++ b/doc/data/messages/d/dict-init-mutate/good.py
@@ -0,0 +1 @@
+fruit_prices = {"apple": 1, "banana": 10}
diff --git a/doc/data/messages/d/dict-init-mutate/pylintrc b/doc/data/messages/d/dict-init-mutate/pylintrc
new file mode 100644
index 000000000..bbe6bd1f7
--- /dev/null
+++ b/doc/data/messages/d/dict-init-mutate/pylintrc
@@ -0,0 +1,2 @@
+[MAIN]
+load-plugins=pylint.extensions.dict_init_mutate,
diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst
index 5b8eca383..0eaf22792 100644
--- a/doc/user_guide/checkers/extensions.rst
+++ b/doc/user_guide/checkers/extensions.rst
@@ -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`
@@ -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
diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst
index c292c88c6..d7c058823 100644
--- a/doc/user_guide/messages/messages_overview.rst
+++ b/doc/user_guide/messages/messages_overview.rst
@@ -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
diff --git a/doc/whatsnew/fragments/2876.new_check b/doc/whatsnew/fragments/2876.new_check
new file mode 100644
index 000000000..a8353a32e
--- /dev/null
+++ b/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
diff --git a/pylint/extensions/dict_init_mutate.py b/pylint/extensions/dict_init_mutate.py
new file mode 100644
index 000000000..fb4c83647
--- /dev/null
+++ b/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))
diff --git a/tests/functional/ext/dict_init_mutate.py b/tests/functional/ext/dict_init_mutate.py
new file mode 100644
index 000000000..f3372bd7e
--- /dev/null
+++ b/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)
diff --git a/tests/functional/ext/dict_init_mutate.rc b/tests/functional/ext/dict_init_mutate.rc
new file mode 100644
index 000000000..bbe6bd1f7
--- /dev/null
+++ b/tests/functional/ext/dict_init_mutate.rc
@@ -0,0 +1,2 @@
+[MAIN]
+load-plugins=pylint.extensions.dict_init_mutate,
diff --git a/tests/functional/ext/dict_init_mutate.txt b/tests/functional/ext/dict_init_mutate.txt
new file mode 100644
index 000000000..c3702491f
--- /dev/null
+++ b/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