summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <pcmanticore@gmail.com>2015-10-27 16:53:27 +0000
committerClaudiu Popa <pcmanticore@gmail.com>2015-10-27 16:53:27 +0000
commit65fb626ade8636a13e1ecafbcc4611ce545cc72a (patch)
treecec85ad3e9b0ef90c2766398fbff441838ceecc8
parentc4e7c46703e54c7dfff51a984db85da1f09abc3c (diff)
downloadpylint-65fb626ade8636a13e1ecafbcc4611ce545cc72a.tar.gz
Add a new convention message, 'consider-using-enumerate'
The message is emitted when code that uses `range` and `len` for iterating is encountered, which can be easily simplified by using `enumerate` instead. This makes the code a bit faster and cleaner. Closes issue #684.
-rw-r--r--ChangeLog4
-rw-r--r--pylint/checkers/base.py69
-rw-r--r--pylint/test/functional/consider_using_enumerate.py43
-rw-r--r--pylint/test/functional/consider_using_enumerate.txt1
4 files changed, 117 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index 5e21556..7f44f14 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -365,6 +365,10 @@ ChangeLog for Pylint
encountered in languages with variabile assignments in conditional
statements.
+ * Add a new convention message, 'consider-using-enumerate', which is
+ emitted when code that uses `range` and `len` for iterating is encountered.
+ Closes issue #684.
+
2015-03-14 -- 1.4.3
diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py
index f368a65..c6f6ac5 100644
--- a/pylint/checkers/base.py
+++ b/pylint/checkers/base.py
@@ -1463,6 +1463,74 @@ class LambdaForComprehensionChecker(_BasicChecker):
self.add_message('deprecated-lambda', node=node)
+class RecommandationChecker(_BasicChecker):
+ 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.')
+ }
+
+ @staticmethod
+ def _is_builtin(node, function):
+ inferred = helpers.safe_infer(node)
+ if not inferred:
+ return False
+ return is_builtin_object(inferred) and inferred.name == function
+
+ @check_messages('consider-using-enumerate')
+ def visit_for(self, node):
+ """Emit a convention whenever range and len are used for indexing."""
+ # Verify that we have a `range(len(...))` 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 len(node.iter.args) != 1:
+ return
+
+ # Is it a proper len call?
+ if not isinstance(node.iter.args[0], astroid.Call):
+ return
+ second_func = node.iter.args[0].func
+ if not self._is_builtin(second_func, 'len'):
+ return
+ len_args = node.iter.args[0].args
+ if not len_args or len(len_args) != 1:
+ return
+ iterating_object = len_args[0]
+ if not isinstance(iterating_object, astroid.Name):
+ 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):
+ if not isinstance(subscript.value, astroid.Name):
+ continue
+ if not isinstance(subscript.slice, astroid.Index):
+ continue
+ if not isinstance(subscript.slice.value, astroid.Name):
+ continue
+ if subscript.slice.value.name != node.target.name:
+ continue
+ if iterating_object.name != subscript.value.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
+ self.add_message('consider-using-enumerate', node=node)
+ return
+
+
def _is_one_arg_pos_call(call):
"""Is this a call with exactly 1 argument,
where that argument is positional?
@@ -1576,3 +1644,4 @@ def register(linter):
linter.register_checker(PassChecker(linter))
linter.register_checker(LambdaForComprehensionChecker(linter))
linter.register_checker(ComparisonChecker(linter))
+ linter.register_checker(RecommandationChecker(linter))
diff --git a/pylint/test/functional/consider_using_enumerate.py b/pylint/test/functional/consider_using_enumerate.py
new file mode 100644
index 0000000..f906da0
--- /dev/null
+++ b/pylint/test/functional/consider_using_enumerate.py
@@ -0,0 +1,43 @@
+"""Emit a message for iteration through range and len is encountered."""
+
+# pylint: disable=missing-docstring, import-error
+
+def bad():
+ iterable = [1, 2, 3]
+ for obj in range(len(iterable)): # [consider-using-enumerate]
+ yield iterable[obj]
+
+
+def good():
+ iterable = other_obj = [1, 2, 3]
+ total = 0
+ for obj in range(len(iterable)):
+ total += obj
+ yield total
+ yield iterable[obj + 1: 2]
+ yield iterable[len(obj)]
+ for obj in iterable:
+ yield iterable[obj - 1]
+
+ for index, obj in enumerate(iterable):
+ yield iterable[index]
+ for index in range(0, 10):
+ yield iterable[index + 1]
+ for index in range(10):
+ yield iterable[index]
+ for index in range(len([1, 2, 3, 4])):
+ yield index
+ for index in range(len(iterable)):
+ yield [1, 2, 3][index]
+ yield len([1, 2, 3])
+ for index in range(len(iterable)):
+ yield other_obj[index]
+
+ from unknown import unknown
+ for index in range(unknown(iterable)):
+ yield iterable[index]
+
+ for index in range(len(iterable)):
+ def test(iterable):
+ return iterable[index]
+ yield test([1, 2, 3])
diff --git a/pylint/test/functional/consider_using_enumerate.txt b/pylint/test/functional/consider_using_enumerate.txt
new file mode 100644
index 0000000..36cd55d
--- /dev/null
+++ b/pylint/test/functional/consider_using_enumerate.txt
@@ -0,0 +1 @@
+consider-using-enumerate:7:bad:Consider using enumerate instead of iterating with range and len