Last active
February 3, 2020 05:48
-
-
Save Tatsh/81dcf0bcb5fdf9edd63966547b9cb6ad to your computer and use it in GitHub Desktop.
Start of a Pylint plugin to check for lists that have never been mutated, and could therefore be a tuple or other immutable type.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import Dict, List, Optional, Union | |
from pylint.checkers import BaseChecker | |
from pylint.interfaces import IAstroidChecker | |
from pylint.lint import PyLinter | |
import astroid | |
from astroid.scoped_nodes import FunctionDef | |
class ListNotModifiedChecker(BaseChecker): # type: ignore[misc] | |
__implements__ = IAstroidChecker | |
name = 'list-not-modified' | |
priority = -1 | |
msgs = { | |
'W0001': | |
('List %r never gets mutated. Consider using an immutable type', | |
'list-not-modified', | |
'It is preferable to use immutable types when possible') | |
} | |
MUTATING_METHODS = { | |
'append', 'clear', 'extend', 'insert', 'pop', 'remove', 'reverse', | |
'sort' | |
} | |
# ops: indexing, slice, del, *= | |
def __init__(self, linter: Optional[PyLinter] = None): | |
super().__init__(linter) | |
self._function_stack: List[FunctionDef] = [] | |
self._lists_being_tracked: Dict[str, List[Union[astroid.List, | |
bool]]] = {} | |
def visit_functiondef(self, node: FunctionDef) -> None: | |
self._function_stack.append(node) | |
def leave_functiondef(self, _node: FunctionDef) -> None: | |
self._function_stack.pop() | |
for name, (node, value) in self._lists_being_tracked.items(): | |
if value is False: | |
self.add_message('list-not-modified', node=node, args=(name, )) | |
self._lists_being_tracked = {} | |
def visit_call(self, node: astroid.Call) -> None: | |
if not self._function_stack or not isinstance(node.func, | |
astroid.Attribute): | |
return | |
if node.func.attrname in self.MUTATING_METHODS: | |
try: | |
key = list( | |
list(node.nodes_of_class( | |
astroid.Attribute))[0].nodes_of_class( | |
astroid.Name))[0].name | |
except AttributeError as e: | |
print('Failed to get name key') | |
raise e | |
try: | |
self._lists_being_tracked[key][1] = True | |
except KeyError: | |
return | |
def visit_subscript(self, node: astroid.Subscript) -> None: | |
if (isinstance(node.parent, astroid.Assign) | |
and isinstance(node.parent.value, astroid.List) | |
and isinstance(node.value, astroid.Name)): | |
self._lists_being_tracked[node.value.name][1] = True | |
def visit_assign(self, node: astroid.Assign) -> None: | |
if not self._function_stack: | |
return | |
root = node.root() | |
if (isinstance(node.value, astroid.List) | |
or (isinstance(node.value, astroid.Call) | |
and isinstance(node.value.func, astroid.Name) | |
and node.value.func.name == 'copy' and 'copy' in root | |
and isinstance(root['copy'], astroid.ImportFrom))): | |
name_assignment = list(node.nodes_of_class(astroid.AssignName)) | |
if not name_assignment: | |
subscript = list(node.nodes_of_class(astroid.Subscript)) | |
if subscript and len(subscript) == 1: | |
return | |
assign_attr = list(node.nodes_of_class(astroid.AssignAttr)) | |
if assign_attr and len(assign_attr) == 1: | |
return | |
print(node.repr_tree()) | |
raise NotImplementedError( | |
'name_assignment empty, subscript check failed, ' | |
f'{node}, {node.value}') | |
else: | |
key = name_assignment[0].name | |
self._lists_being_tracked[key] = [node, False] | |
visit_annassign = visit_assign | |
def register(linter: PyLinter) -> None: | |
linter.register_checker(ListNotModifiedChecker(linter)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from copy import copy | |
def test1(): | |
x = [] # error | |
return x | |
def test2(): | |
y = [] # ok | |
y.append(2) | |
return y | |
def test3(): | |
y = [] # ok | |
y.clear() | |
return y | |
def test4(): | |
z = copy([]) # error | |
return z | |
def test5(): | |
z = [1, 2, 3, 4, 5] # ok | |
z[1:2] = [10, 11] | |
return z | |
class Test6: | |
def test6(self): | |
z = [] # error | |
return z | |
@classmethod | |
def test7(self): | |
z = [] # error | |
return z |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment