| # Copyright (c) 2016-2018, 2020 Claudiu Popa <pcmanticore@gmail.com> |
| # Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com> |
| # Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com> |
| # Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi> |
| # Copyright (c) 2019, 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com> |
| # Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com> |
| # Copyright (c) 2020 Anthony Sottile <asottile@umich.edu> |
| |
| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| # For details: https://github.com/PyCQA/pylint/blob/master/LICENSE |
| |
| import astroid |
| |
| from pylint.checkers import BaseChecker |
| from pylint.checkers.utils import check_messages, is_none, node_type |
| from pylint.interfaces import IAstroidChecker |
| |
| BUILTINS = "builtins" |
| |
| |
| class MultipleTypesChecker(BaseChecker): |
| """Checks for variable type redefinitions (NoneType excepted) |
| |
| At a function, method, class or module scope |
| |
| This rule could be improved: |
| |
| - Currently, if an attribute is set to different types in 2 methods of a |
| same class, it won't be detected (see functional test) |
| - One could improve the support for inference on assignment with tuples, |
| ifexpr, etc. Also it would be great to have support for inference on |
| str.split() |
| """ |
| |
| __implements__ = IAstroidChecker |
| |
| name = "multiple_types" |
| msgs = { |
| "R0204": ( |
| "Redefinition of %s type from %s to %s", |
| "redefined-variable-type", |
| "Used when the type of a variable changes inside a " |
| "method or a function.", |
| ) |
| } |
| |
| def visit_classdef(self, _): |
| self._assigns.append({}) |
| |
| @check_messages("redefined-variable-type") |
| def leave_classdef(self, _): |
| self._check_and_add_messages() |
| |
| visit_functiondef = visit_classdef |
| leave_functiondef = leave_module = leave_classdef |
| |
| def visit_module(self, _): |
| self._assigns = [{}] |
| |
| def _check_and_add_messages(self): |
| assigns = self._assigns.pop() |
| for name, args in assigns.items(): |
| if len(args) <= 1: |
| continue |
| orig_node, orig_type = args[0] |
| # Check if there is a type in the following nodes that would be |
| # different from orig_type. |
| for redef_node, redef_type in args[1:]: |
| if redef_type == orig_type: |
| continue |
| # if a variable is defined to several types in an if node, |
| # this is not actually redefining. |
| orig_parent = orig_node.parent |
| redef_parent = redef_node.parent |
| if isinstance(orig_parent, astroid.If): |
| if orig_parent == redef_parent: |
| if ( |
| redef_node in orig_parent.orelse |
| and orig_node not in orig_parent.orelse |
| ): |
| orig_node, orig_type = redef_node, redef_type |
| continue |
| elif isinstance( |
| redef_parent, astroid.If |
| ) and redef_parent in orig_parent.nodes_of_class(astroid.If): |
| orig_node, orig_type = redef_node, redef_type |
| continue |
| orig_type = orig_type.replace(BUILTINS + ".", "") |
| redef_type = redef_type.replace(BUILTINS + ".", "") |
| self.add_message( |
| "redefined-variable-type", |
| node=redef_node, |
| args=(name, orig_type, redef_type), |
| ) |
| break |
| |
| def visit_assign(self, node): |
| # we don't handle multiple assignment nor slice assignment |
| target = node.targets[0] |
| if isinstance(target, (astroid.Tuple, astroid.Subscript)): |
| return |
| # ignore NoneType |
| if is_none(node): |
| return |
| _type = node_type(node.value) |
| if _type: |
| self._assigns[-1].setdefault(target.as_string(), []).append( |
| (node, _type.pytype()) |
| ) |
| |
| |
| def register(linter): |
| """Required method to auto register this checker. |
| |
| :param linter: Main interface object for Pylint plugins |
| :type linter: Pylint object |
| """ |
| linter.register_checker(MultipleTypesChecker(linter)) |