From f7d681b5a79e5781ab8072fe64459b199955a1f6 Mon Sep 17 00:00:00 2001 From: Dani Alcala <112832187+clavedeluna@users.noreply.github.com> Date: Wed, 23 Nov 2022 14:11:20 -0300 Subject: Add a new check `dict-init-mutate` (#7794) Co-authored-by: Pierre Sassoulas --- doc/data/messages/d/dict-init-mutate/bad.py | 3 ++ doc/data/messages/d/dict-init-mutate/good.py | 1 + doc/data/messages/d/dict-init-mutate/pylintrc | 2 + doc/user_guide/checkers/extensions.rst | 16 +++++++ doc/user_guide/messages/messages_overview.rst | 1 + doc/whatsnew/fragments/2876.new_check | 4 ++ pylint/extensions/dict_init_mutate.py | 66 +++++++++++++++++++++++++++ tests/functional/ext/dict_init_mutate.py | 38 +++++++++++++++ tests/functional/ext/dict_init_mutate.rc | 2 + tests/functional/ext/dict_init_mutate.txt | 3 ++ 10 files changed, 136 insertions(+) create mode 100644 doc/data/messages/d/dict-init-mutate/bad.py create mode 100644 doc/data/messages/d/dict-init-mutate/good.py create mode 100644 doc/data/messages/d/dict-init-mutate/pylintrc create mode 100644 doc/whatsnew/fragments/2876.new_check create mode 100644 pylint/extensions/dict_init_mutate.py create mode 100644 tests/functional/ext/dict_init_mutate.py create mode 100644 tests/functional/ext/dict_init_mutate.rc create mode 100644 tests/functional/ext/dict_init_mutate.txt 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 -- cgit v1.2.1