diff options
Diffstat (limited to 'pylint/checkers/typecheck.py')
-rw-r--r-- | pylint/checkers/typecheck.py | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 66ac05b..80b51b5 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -44,6 +44,10 @@ _ZOPE_DEPRECATED = ( ) BUILTINS = six.moves.builtins.__name__ STR_FORMAT = "%s.str.format" % BUILTINS +ITER_METHOD = '__iter__' +NEXT_METHOD = 'next' if six.PY2 else '__next__' +GETITEM_METHOD = '__getitem__' +KEYS_METHOD = 'keys' def _unflatten(iterable): @@ -84,6 +88,44 @@ def _is_owner_ignored(owner, name, ignored_classes, ignored_modules): return any(name == ignore or qname == ignore for ignore in ignored_classes) +def _hasattr(value, attr): + try: + value.getattr(attr) + return True + except astroid.NotFoundError: + return False + +def _is_comprehension(node): + comprehensions = (astroid.ListComp, + astroid.SetComp, + astroid.DictComp) + return isinstance(node, comprehensions) + + +def _is_iterable(value): + # '__iter__' is for standard iterables + # '__getitem__' is for strings and other old-style iterables + return _hasattr(value, ITER_METHOD) or _hasattr(value, GETITEM_METHOD) + + +def _is_iterator(value): + return _hasattr(value, NEXT_METHOD) and _hasattr(value, ITER_METHOD) + + +def _is_mapping(value): + return _hasattr(value, GETITEM_METHOD) and _hasattr(value, KEYS_METHOD) + + +def _is_inside_mixin_declaration(node): + while node is not None: + if isinstance(node, astroid.ClassDef): + name = getattr(node, 'name', None) + if name is not None and name.lower().endswith("mixin"): + return True + node = node.parent + return False + + MSGS = { 'E1101': ('%s %r has no %r member', 'no-member', @@ -791,6 +833,124 @@ accessed. Python regular expressions are accepted.'} args=str(error), node=node) +class IterableChecker(BaseChecker): + """ + Checks for non-iterables used in an iterable context. + Contexts include: + - for-statement + - starargs in function call + - `yield from`-statement + - list, dict and set comprehensions + - generator expressions + Also checks for non-mappings in function call kwargs. + """ + + __implements__ = (IAstroidChecker,) + name = 'iterable_check' + + msgs = {'E1132': ('Non-iterable value %s is used in an iterating context', + 'not-an-iterable', + 'Used when a non-iterable value is used in place where' + 'iterable is expected'), + 'E1133': ('Non-mapping value %s is used in a mapping context', + 'not-a-mapping', + 'Used when a non-mapping value is used in place where' + 'mapping is expected'), + } + + def _check_iterable(self, node, root_node): + # for/set/dict-comprehensions can't be infered with astroid + # so we have to check for them explicitly + if _is_comprehension(node) or _is_inside_mixin_declaration(node): + return + + infered = helpers.safe_infer(node) + if infered is None or infered is astroid.YES: + return + + if isinstance(infered, astroid.ClassDef): + if not helpers.has_known_bases(infered): + return + # classobj can only be iterable if it has an iterable metaclass + meta = infered.metaclass() + if meta is not None: + if _is_iterable(meta): + return + if _is_iterator(meta): + return + + if isinstance(infered, astroid.Instance): + if not helpers.has_known_bases(infered): + return + if _is_iterable(infered) or _is_iterator(infered): + return + + self.add_message('not-an-iterable', + args=node.as_string(), + node=root_node) + + def _check_mapping(self, node, root_node): + if isinstance(node, astroid.DictComp) or _is_inside_mixin_declaration(node): + return + + infered = helpers.safe_infer(node) + if infered is None or infered is astroid.YES: + return + + if isinstance(infered, astroid.ClassDef): + if not helpers.has_known_bases(infered): + return + meta = infered.metaclass() + if meta is not None and _is_mapping(meta): + return + + if isinstance(infered, astroid.Instance): + if not helpers.has_known_bases(infered): + return + if _is_mapping(infered): + return + + self.add_message('not-a-mapping', + args=node.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) + + @check_messages('not-an-iterable') + def visit_listcomp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + + @check_messages('not-an-iterable') + def visit_dictcomp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + + @check_messages('not-an-iterable') + def visit_setcomp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + + @check_messages('not-an-iterable') + def visit_generatorexp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + + def register(linter): """required method to auto register this checker """ linter.register_checker(TypeChecker(linter)) + linter.register_checker(IterableChecker(linter)) |