blob: 6ea38712653626c569cb28f7b3ab49afd9783b0b [file] [log] [blame]
# 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
from typing import cast
import astroid
from pylint import checkers, interfaces
from pylint.checkers import utils
class RecommendationChecker(checkers.BaseChecker):
__implements__ = (interfaces.IAstroidChecker,)
name = "refactoring"
msgs = {
"C0200": (
"Consider using enumerate instead of iterating with range and len",
"consider-using-enumerate",
"Emitted when code that iterates with range and len is "
"encountered. Such code can be simplified by using the "
"enumerate builtin.",
),
"C0201": (
"Consider iterating the dictionary directly instead of calling .keys()",
"consider-iterating-dictionary",
"Emitted when the keys of a dictionary are iterated through the .keys() "
"method. It is enough to just iterate through the dictionary itself, as "
'in "for key in dictionary".',
),
"C0206": (
"Consider iterating with .items()",
"consider-using-dict-items",
"Emitted when iterating over the keys of a dictionary and accessing the "
"value by index lookup. "
"Both the key and value can be accessed by iterating using the .items() "
"method of the dictionary instead.",
),
"C0207": (
"Use %s instead",
"use-maxsplit-arg",
"Emitted when accessing only the first or last element of str.split(). "
"The first and last element can be accessed by using "
"str.split(sep, maxsplit=1)[0] or str.rsplit(sep, maxsplit=1)[-1] "
"instead.",
),
}
@staticmethod
def _is_builtin(node, function):
inferred = utils.safe_infer(node)
if not inferred:
return False
return utils.is_builtin_object(inferred) and inferred.name == function
@utils.check_messages("consider-iterating-dictionary", "use-maxsplit-arg")
def visit_call(self, node: astroid.Call) -> None:
self._check_consider_iterating_dictionary(node)
self._check_use_maxsplit_arg(node)
def _check_consider_iterating_dictionary(self, node: astroid.Call) -> None:
if not isinstance(node.func, astroid.Attribute):
return
if node.func.attrname != "keys":
return
if not isinstance(node.parent, (astroid.For, astroid.Comprehension)):
return
inferred = utils.safe_infer(node.func)
if not isinstance(inferred, astroid.BoundMethod) or not isinstance(
inferred.bound, astroid.Dict
):
return
if isinstance(node.parent, (astroid.For, astroid.Comprehension)):
self.add_message("consider-iterating-dictionary", node=node)
def _check_use_maxsplit_arg(self, node: astroid.Call) -> None:
"""Add message when accessing first or last elements of a str.split() or str.rsplit()."""
# Check if call is split() or rsplit()
if (
isinstance(node.func, astroid.Attribute)
and node.func.attrname in ("split", "rsplit")
and isinstance(utils.safe_infer(node.func), astroid.BoundMethod)
):
try:
utils.get_argument_from_call(node, 0, "sep")
except utils.NoSuchArgumentError:
return
try:
# Ignore if maxsplit arg has been set
utils.get_argument_from_call(node, 1, "maxsplit")
return
except utils.NoSuchArgumentError:
pass
if isinstance(node.parent, astroid.Subscript):
try:
subscript_value = utils.get_subscript_const_value(node.parent).value
except utils.InferredTypeError:
return
if subscript_value in (-1, 0):
fn_name = node.func.attrname
new_fn = "rsplit" if subscript_value == -1 else "split"
new_name = (
node.func.as_string().rsplit(fn_name, maxsplit=1)[0]
+ new_fn
+ f"({node.args[0].as_string()}, maxsplit=1)[{subscript_value}]"
)
self.add_message("use-maxsplit-arg", node=node, args=(new_name,))
@utils.check_messages("consider-using-enumerate", "consider-using-dict-items")
def visit_for(self, node: astroid.For) -> None:
self._check_consider_using_enumerate(node)
self._check_consider_using_dict_items(node)
def _check_consider_using_enumerate(self, node: astroid.For) -> None:
"""Emit a convention whenever range and len are used for indexing."""
# Verify that we have a `range([start], len(...), [stop])` call and
# that the object which is iterated is used as a subscript in the
# body of the for.
# Is it a proper range call?
if not isinstance(node.iter, astroid.Call):
return
if not self._is_builtin(node.iter.func, "range"):
return
if not node.iter.args:
return
is_constant_zero = (
isinstance(node.iter.args[0], astroid.Const)
and node.iter.args[0].value == 0
)
if len(node.iter.args) == 2 and not is_constant_zero:
return
if len(node.iter.args) > 2:
return
# Is it a proper len call?
if not isinstance(node.iter.args[-1], astroid.Call):
return
second_func = node.iter.args[-1].func
if not self._is_builtin(second_func, "len"):
return
len_args = node.iter.args[-1].args
if not len_args or len(len_args) != 1:
return
iterating_object = len_args[0]
if isinstance(iterating_object, astroid.Name):
expected_subscript_val_type = astroid.Name
elif isinstance(iterating_object, astroid.Attribute):
expected_subscript_val_type = astroid.Attribute
else:
return
# If we're defining __iter__ on self, enumerate won't work
scope = node.scope()
if (
isinstance(iterating_object, astroid.Name)
and iterating_object.name == "self"
and scope.name == "__iter__"
):
return
# Verify that the body of the for loop uses a subscript
# with the object that was iterated. This uses some heuristics
# in order to make sure that the same object is used in the
# for body.
for child in node.body:
for subscript in child.nodes_of_class(astroid.Subscript):
subscript = cast(astroid.Subscript, subscript)
if not isinstance(subscript.value, expected_subscript_val_type):
continue
value = subscript.slice
if not isinstance(value, astroid.Name):
continue
if subscript.value.scope() != node.scope():
# Ignore this subscript if it's not in the same
# scope. This means that in the body of the for
# loop, another scope was created, where the same
# name for the iterating object was used.
continue
if value.name == node.target.name and (
isinstance(subscript.value, astroid.Name)
and iterating_object.name == subscript.value.name
or isinstance(subscript.value, astroid.Attribute)
and iterating_object.attrname == subscript.value.attrname
):
self.add_message("consider-using-enumerate", node=node)
return
def _check_consider_using_dict_items(self, node: astroid.For) -> None:
"""Add message when accessing dict values by index lookup."""
# Verify that we have a .keys() call and
# that the object which is iterated is used as a subscript in the
# body of the for.
iterating_object_name = utils.get_iterating_dictionary_name(node)
if iterating_object_name is None:
return
# Verify that the body of the for loop uses a subscript
# with the object that was iterated. This uses some heuristics
# in order to make sure that the same object is used in the
# for body.
for child in node.body:
for subscript in child.nodes_of_class(astroid.Subscript):
subscript = cast(astroid.Subscript, subscript)
if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)):
continue
value = subscript.slice
if (
not isinstance(value, astroid.Name)
or value.name != node.target.name
or iterating_object_name != subscript.value.as_string()
):
continue
last_definition_lineno = value.lookup(value.name)[1][-1].lineno
if last_definition_lineno > node.lineno:
# Ignore this subscript if it has been redefined after
# the for loop. This checks for the line number using .lookup()
# to get the line number where the iterating object was last
# defined and compare that to the for loop's line number
continue
if (
isinstance(subscript.parent, astroid.Assign)
and subscript in subscript.parent.targets
or isinstance(subscript.parent, astroid.AugAssign)
and subscript == subscript.parent.target
):
# Ignore this subscript if it is the target of an assignment
continue
self.add_message("consider-using-dict-items", node=node)
return
@utils.check_messages("consider-using-dict-items")
def visit_comprehension(self, node: astroid.Comprehension) -> None:
iterating_object_name = utils.get_iterating_dictionary_name(node)
if iterating_object_name is None:
return
for child in node.parent.get_children():
for subscript in child.nodes_of_class(astroid.Subscript):
subscript = cast(astroid.Subscript, subscript)
if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)):
continue
value = subscript.slice
if (
not isinstance(value, astroid.Name)
or value.name != node.target.name
or iterating_object_name != subscript.value.as_string()
):
continue
self.add_message("consider-using-dict-items", node=node)
return