diff options
author | Dmitry Pribysh <dmand@yandex.ru> | 2015-10-05 17:15:14 +0300 |
---|---|---|
committer | Dmitry Pribysh <dmand@yandex.ru> | 2015-10-05 17:15:14 +0300 |
commit | aba38cd9c0cd8be923a7aa8e0d1712c53d3e1e6c (patch) | |
tree | 04a9ab0a02dc0087ec55e66dacb68f21cfd69edd | |
parent | 4b887f72eb67cc6f3324c396c826008e34476959 (diff) | |
download | pylint-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.py | 58 | ||||
-rw-r--r-- | pylint/test/unittest_checker_base.py | 107 |
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() |