summaryrefslogtreecommitdiff
path: root/pylint/checkers/typecheck.py
diff options
context:
space:
mode:
Diffstat (limited to 'pylint/checkers/typecheck.py')
-rw-r--r--pylint/checkers/typecheck.py160
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))