summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Pribysh <dmand@yandex.ru>2015-10-05 17:15:14 +0300
committerDmitry Pribysh <dmand@yandex.ru>2015-10-05 17:15:14 +0300
commitaba38cd9c0cd8be923a7aa8e0d1712c53d3e1e6c (patch)
tree04a9ab0a02dc0087ec55e66dacb68f21cfd69edd
parent4b887f72eb67cc6f3324c396c826008e34476959 (diff)
downloadpylint-aba38cd9c0cd8be923a7aa8e0d1712c53d3e1e6c.tar.gz
Add initial version of checker for iterables/mappings.
It checks for the following things: - for-statement should contain an iterable value - `yield from`-statement should contain an iterable value - function call with star args should contain iterable value (e.g. in `foo(*bar)` bar should be an iterable) - function call with kwargs should contain a mapping (e.g. in `foo(**bar)` bar should be a dict) Idea came from issue #563.
-rw-r--r--pylint/checkers/base.py58
-rw-r--r--pylint/test/unittest_checker_base.py107
2 files changed, 165 insertions, 0 deletions
diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py
index 30e337b..84f5a8d 100644
--- a/pylint/checkers/base.py
+++ b/pylint/checkers/base.py
@@ -1476,6 +1476,64 @@ class ComparisonChecker(_BasicChecker):
self.check_singleton_comparison(right, node)
+class IterableChecker(_BasicChecker):
+ """
+ TODO:
+ """
+ msgs = {'E0121': ('Non-iterable value %s is used in an iterating context',
+ 'not-an-iterable',
+ 'YOLO'),
+ 'E0122': ('Non-mapping value %s is used in an mapping context',
+ 'not-a-mapping',
+ 'YOLO'),
+ }
+
+ def _is_string(self, node):
+ return isinstance(node, astroid.Const) and \
+ isinstance(node.value, six.string_types)
+
+ def _is_iterable(self, value):
+ is_iterable_value = isinstance(value, (astroid.List, astroid.Tuple,
+ astroid.Set, astroid.Dict,
+ astroid.bases.Generator))
+ is_string = self._is_string(value)
+ return is_iterable_value or is_string
+
+ def _is_mapping(self, value):
+ return isinstance(value, astroid.Dict)
+
+ def _check_iterable(self, element, root_node):
+ infered = helpers.safe_infer(element)
+ if infered is not None:
+ if not self._is_iterable(infered):
+ self.add_message('not-an-iterable',
+ args=infered.as_string(),
+ node=root_node)
+
+ def _check_mapping(self, element, root_node):
+ infered = helpers.safe_infer(element)
+ if infered is not None:
+ if not self._is_mapping(infered):
+ self.add_message('not-a-mapping',
+ args=infered.as_string(),
+ node=root_node)
+
+ @check_messages('not-an-iterable')
+ def visit_for(self, node):
+ self._check_iterable(node.iter, node)
+
+ @check_messages('not-an-iterable')
+ def visit_yieldfrom(self, node):
+ self._check_iterable(node.value, node)
+
+ @check_messages('not-an-iterable', 'not-a-mapping')
+ def visit_call(self, node):
+ for stararg in node.starargs:
+ self._check_iterable(stararg.value, node)
+ for kwarg in node.kwargs:
+ self._check_mapping(kwarg.value, node)
+
+
def register(linter):
"""required method to auto register this checker"""
linter.register_checker(BasicErrorChecker(linter))
diff --git a/pylint/test/unittest_checker_base.py b/pylint/test/unittest_checker_base.py
index c68f379..fb595e3 100644
--- a/pylint/test/unittest_checker_base.py
+++ b/pylint/test/unittest_checker_base.py
@@ -10,6 +10,13 @@ from pylint.checkers import base
from pylint.testutils import CheckerTestCase, Message, set_config
+def python33_and_newer(test):
+ """
+ Decorator for any tests that will fail if launched not with Python 3.3+.
+ """
+ return unittest.skipIf(sys.version_info < (3, 3),
+ 'Python 3.2 and older')(test)
+
class DocstringTest(CheckerTestCase):
CHECKER_CLASS = base.DocStringChecker
@@ -293,5 +300,105 @@ class ComparisonTest(CheckerTestCase):
self.checker.visit_compare(node)
+class IterableTest(CheckerTestCase):
+ CHECKER_CLASS = base.IterableChecker
+
+ def test_non_iterable_in_for(self):
+ node = test_utils.extract_node("""
+ for i in 42:
+ print(i)
+ """)
+ message = Message('not-an-iterable', node=node, args='42')
+ with self.assertAddsMessages(message):
+ self.checker.visit_for(node)
+
+ node = test_utils.extract_node("""
+ for i in [1,2,3]:
+ print(i)
+ """)
+ with self.assertNoMessages():
+ self.checker.visit_for(node)
+
+ node = test_utils.extract_node("""
+ def count():
+ i = 0
+ while True:
+ yield i
+ i += 1
+
+ for i in count():
+ print(i)
+ """)
+ with self.assertNoMessages():
+ self.walk(node.root())
+
+ node = test_utils.extract_node("""
+ for i in "aeiou":
+ print(i)
+ """)
+ with self.assertNoMessages():
+ self.checker.visit_for(node)
+
+ @python33_and_newer
+ def test_non_iterable_in_yield_from(self):
+ node = test_utils.extract_node("""
+ yield from 42
+ """)
+ message = Message('not-an-iterable', node=node, args='42')
+ with self.assertAddsMessages(message):
+ self.checker.visit_yieldfrom(node)
+
+ node = test_utils.extract_node("""
+ yield from [1,2,3]
+ """)
+ with self.assertNoMessages():
+ self.checker.visit_yieldfrom(node)
+
+ def test_non_iterable_in_starargs(self):
+ node = test_utils.extract_node("""
+ foo(*123)
+ """)
+ message = Message('not-an-iterable', node=node, args='123')
+ with self.assertAddsMessages(message):
+ self.checker.visit_call(node)
+
+ node = test_utils.extract_node("""
+ stuff = [1,2,3]
+ foo(*stuff)
+ """)
+ with self.assertNoMessages():
+ self.walk(node.root())
+
+ def test_non_mapping_in_kwargs(self):
+ node = test_utils.extract_node("""
+ foo(**123)
+ """)
+ message = Message('not-a-mapping', node=node, args='123')
+ with self.assertAddsMessages(message):
+ self.checker.visit_call(node)
+
+ node = test_utils.extract_node("""
+ args = [1, 2, 3]
+ foo(**args)
+ """)
+ message = Message('not-a-mapping', node=node, args='[1, 2, 3]')
+ with self.assertAddsMessages(message):
+ self.walk(node.root())
+
+ node = test_utils.extract_node("""
+ kwargs = {'answer': 42}
+ foo(**kwargs)
+ """)
+ with self.assertNoMessages():
+ self.walk(node.root())
+
+ node = test_utils.extract_node("""
+ foo(**dict(a=1, b=2))
+ """)
+ with self.assertNoMessages():
+ self.checker.visit_call(node)
+
+
+
if __name__ == '__main__':
unittest.main()