Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Tatsh
Last active February 3, 2020 05:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Tatsh/81dcf0bcb5fdf9edd63966547b9cb6ad to your computer and use it in GitHub Desktop.
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.
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))
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