summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Pribysh <dmand@yandex.ru>2015-10-19 17:44:10 +0300
committerDmitry Pribysh <dmand@yandex.ru>2015-10-19 17:44:10 +0300
commit816d3212d15ba297aa3a234d7874974d9f39b4bd (patch)
tree2a55cd5bb12fc49ec47919c926dca12ebf2aa3ba
parent5e649a112f60f3c330b5f7544b6e6286f983beca (diff)
downloadpylint-membership-test-checker.tar.gz
Add checker for membership rules and testsmembership-test-checker
Idea of checking is very simple: value used to the right of the 'in' operator should support membership test protocol (i.e. define either __contains__ or __iter__ or __getitem__). Fixes issue #589.
-rw-r--r--ChangeLog4
-rw-r--r--pylint/checkers/typecheck.py48
-rw-r--r--pylint/test/functional/membership_protocol.py85
-rw-r--r--pylint/test/functional/membership_protocol.txt7
-rw-r--r--pylint/test/functional/membership_protocol_py2.py36
-rw-r--r--pylint/test/functional/membership_protocol_py2.rc3
-rw-r--r--pylint/test/functional/membership_protocol_py2.txt3
-rw-r--r--pylint/test/functional/membership_protocol_py3.py36
-rw-r--r--pylint/test/functional/membership_protocol_py3.rc3
-rw-r--r--pylint/test/functional/membership_protocol_py3.txt3
10 files changed, 228 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index b25533b..957ed56 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,6 +2,10 @@ ChangeLog for Pylint
--------------------
--
+ * Add a new error, 'unsupported-membership-test', emitted when value
+ to the right of the 'in' operator doesn't support membership test
+ protocol (i.e. doesn't define __contains__/__iter__/__getitem__)
+
* Add new errors, 'not-an-iterable', emitted when non-iterable value
is used in an iterating context (starargs, for-statement,
comprehensions, etc), and 'not-a-mapping', emitted when non-mapping
diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py
index 687dd79..e9fddbd 100644
--- a/pylint/checkers/typecheck.py
+++ b/pylint/checkers/typecheck.py
@@ -47,6 +47,7 @@ STR_FORMAT = "%s.str.format" % BUILTINS
ITER_METHOD = '__iter__'
NEXT_METHOD = 'next' if six.PY2 else '__next__'
GETITEM_METHOD = '__getitem__'
+CONTAINS_METHOD = '__contains__'
KEYS_METHOD = 'keys'
@@ -115,6 +116,9 @@ def _is_iterator(value):
def _is_mapping(value):
return _hasattr(value, GETITEM_METHOD) and _hasattr(value, KEYS_METHOD)
+def _supports_membership_test(value):
+ return _hasattr(value, CONTAINS_METHOD)
+
def _is_inside_mixin_declaration(node):
while node is not None:
@@ -189,6 +193,10 @@ MSGS = {
'E1132': ('Got multiple values for keyword argument %r in function call',
'repeated-keyword',
'Emitted when a function call got multiple values for a keyword.'),
+ 'E1135': ("Value '%s' doesn't support membership test",
+ 'unsupported-membership-test',
+ 'Emitted when an instance in membership test expression doesn\'t'
+ 'implement membership protocol (__contains__/__iter__/__getitem__)'),
}
# builtin sequence types in Python 2 and 3.
@@ -832,6 +840,46 @@ accessed. Python regular expressions are accepted.'}
self.add_message('unsupported-binary-operation',
args=str(error), node=node)
+ def _check_membership_test(self, node):
+ # instance supports membership test in either of those cases:
+ # 1. instance defines __contains__ method
+ # 2. instance is iterable (defines __iter__ or __getitem__)
+ 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
+
+ # classes can be iterables/containers too
+ if isinstance(infered, astroid.ClassDef):
+ if not helpers.has_known_bases(infered):
+ return
+ meta = infered.metaclass()
+ if meta is not None:
+ if _supports_membership_test(meta):
+ return
+ if _is_iterable(meta):
+ return
+
+ if isinstance(infered, astroid.Instance):
+ if not helpers.has_known_bases(infered):
+ return
+ if _supports_membership_test(infered) or _is_iterable(infered):
+ return
+
+ self.add_message('unsupported-membership-test',
+ args=node.as_string(),
+ node=node)
+
+ @check_messages('unsupported-membership-test')
+ def visit_compare(self, node):
+ if len(node.ops) != 1:
+ return
+ operator, right = node.ops[0]
+ if operator in ['in', 'not in']:
+ self._check_membership_test(right)
+
class IterableChecker(BaseChecker):
"""
diff --git a/pylint/test/functional/membership_protocol.py b/pylint/test/functional/membership_protocol.py
new file mode 100644
index 0000000..7b3a46f
--- /dev/null
+++ b/pylint/test/functional/membership_protocol.py
@@ -0,0 +1,85 @@
+# pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,too-few-public-methods,import-error,no-init
+
+# standard types
+1 in [1, 2, 3]
+1 in {'a': 1, 'b': 2}
+1 in {1, 2, 3}
+1 in (1, 2, 3)
+1 in "123"
+1 in u"123"
+1 in bytearray(b"123")
+1 in frozenset([1, 2, 3])
+
+# comprehensions
+1 in [x ** 2 % 10 for x in range(10)]
+1 in {x ** 2 % 10 for x in range(10)}
+1 in {x: x ** 2 % 10 for x in range(10)}
+
+# iterators
+1 in iter([1, 2, 3])
+
+# generator
+def count(upto=float("inf")):
+ i = 0
+ while True:
+ if i > upto:
+ break
+ yield i
+ i += 1
+
+10 in count(upto=10)
+
+# custom instance
+class UniversalContainer(object):
+ def __contains__(self, key):
+ return True
+
+42 in UniversalContainer()
+
+# custom iterable
+class CustomIterable(object):
+ def __iter__(self):
+ return iter((1, 2, 3))
+3 in CustomIterable()
+
+# old-style iterable
+class OldStyleIterable(object):
+ def __getitem__(self, key):
+ if key < 10:
+ return 2 ** key
+ else:
+ raise IndexError("bad index")
+64 in OldStyleIterable()
+
+# do not emit warning if class has unknown bases
+from some_missing_module import ImportedClass
+
+class MaybeIterable(ImportedClass):
+ pass
+
+10 in MaybeIterable()
+
+# do not emit warning inside mixins
+class UsefulMixin(object):
+ stuff = None
+
+ def get_stuff(self):
+ return self.stuff
+
+ def act(self, thing):
+ stuff = self.get_stuff()
+ if thing in stuff:
+ pass
+
+# error cases
+42 in 42 # [unsupported-membership-test]
+42 not in None # [unsupported-membership-test]
+42 in 8.5 # [unsupported-membership-test]
+
+class EmptyClass(object):
+ pass
+
+42 not in EmptyClass() # [unsupported-membership-test]
+42 in EmptyClass # [unsupported-membership-test]
+42 not in count # [unsupported-membership-test]
+42 in range # [unsupported-membership-test]
diff --git a/pylint/test/functional/membership_protocol.txt b/pylint/test/functional/membership_protocol.txt
new file mode 100644
index 0000000..6e9bd8e
--- /dev/null
+++ b/pylint/test/functional/membership_protocol.txt
@@ -0,0 +1,7 @@
+unsupported-membership-test:75::Value '42' doesn't support membership test
+unsupported-membership-test:76::Value 'None' doesn't support membership test
+unsupported-membership-test:77::Value '8.5' doesn't support membership test
+unsupported-membership-test:82::Value 'EmptyClass()' doesn't support membership test
+unsupported-membership-test:83::Value 'EmptyClass' doesn't support membership test
+unsupported-membership-test:84::Value 'count' doesn't support membership test
+unsupported-membership-test:85::Value 'range' doesn't support membership test
diff --git a/pylint/test/functional/membership_protocol_py2.py b/pylint/test/functional/membership_protocol_py2.py
new file mode 100644
index 0000000..1a01637
--- /dev/null
+++ b/pylint/test/functional/membership_protocol_py2.py
@@ -0,0 +1,36 @@
+# pylint: disable=missing-docstring,too-few-public-methods,no-init,no-self-use,unused-argument,pointless-statement,expression-not-assigned,undefined-variable
+
+# metaclasses that support membership test protocol
+class MetaIterable(type):
+ def __iter__(cls):
+ return iter((1, 2, 3))
+
+class MetaOldIterable(type):
+ def __getitem__(cls, key):
+ if key < 10:
+ return key ** 2
+ else:
+ raise IndexError("bad index")
+
+class MetaContainer(type):
+ def __contains__(cls, key):
+ return False
+
+
+class IterableClass(object):
+ __metaclass__ = MetaIterable
+
+class OldIterableClass(object):
+ __metaclass__ = MetaOldIterable
+
+class ContainerClass(object):
+ __metaclass__ = MetaContainer
+
+
+def test():
+ 1 in IterableClass
+ 1 in OldIterableClass
+ 1 in ContainerClass
+ 1 in IterableClass() # [unsupported-membership-test]
+ 1 in OldIterableClass() # [unsupported-membership-test]
+ 1 in ContainerClass() # [unsupported-membership-test]
diff --git a/pylint/test/functional/membership_protocol_py2.rc b/pylint/test/functional/membership_protocol_py2.rc
new file mode 100644
index 0000000..c78f32f
--- /dev/null
+++ b/pylint/test/functional/membership_protocol_py2.rc
@@ -0,0 +1,3 @@
+[testoptions]
+max_pyver=3.0
+
diff --git a/pylint/test/functional/membership_protocol_py2.txt b/pylint/test/functional/membership_protocol_py2.txt
new file mode 100644
index 0000000..4ba7575
--- /dev/null
+++ b/pylint/test/functional/membership_protocol_py2.txt
@@ -0,0 +1,3 @@
+unsupported-membership-test:34:test:Value 'IterableClass()' doesn't support membership test
+unsupported-membership-test:35:test:Value 'OldIterableClass()' doesn't support membership test
+unsupported-membership-test:36:test:Value 'ContainerClass()' doesn't support membership test
diff --git a/pylint/test/functional/membership_protocol_py3.py b/pylint/test/functional/membership_protocol_py3.py
new file mode 100644
index 0000000..6a77f20
--- /dev/null
+++ b/pylint/test/functional/membership_protocol_py3.py
@@ -0,0 +1,36 @@
+# pylint: disable=missing-docstring,too-few-public-methods,no-init,no-self-use,unused-argument,pointless-statement,expression-not-assigned
+
+# metaclasses that support membership test protocol
+class MetaIterable(type):
+ def __iter__(cls):
+ return iter((1, 2, 3))
+
+class MetaOldIterable(type):
+ def __getitem__(cls, key):
+ if key < 10:
+ return key ** 2
+ else:
+ raise IndexError("bad index")
+
+class MetaContainer(type):
+ def __contains__(cls, key):
+ return False
+
+
+class IterableClass(metaclass=MetaOldIterable):
+ pass
+
+class OldIterableClass(metaclass=MetaOldIterable):
+ pass
+
+class ContainerClass(metaclass=MetaContainer):
+ pass
+
+
+def test():
+ 1 in IterableClass
+ 1 in OldIterableClass
+ 1 in ContainerClass
+ 1 in IterableClass() # [unsupported-membership-test]
+ 1 in OldIterableClass() # [unsupported-membership-test]
+ 1 in ContainerClass() # [unsupported-membership-test]
diff --git a/pylint/test/functional/membership_protocol_py3.rc b/pylint/test/functional/membership_protocol_py3.rc
new file mode 100644
index 0000000..9bf6df0
--- /dev/null
+++ b/pylint/test/functional/membership_protocol_py3.rc
@@ -0,0 +1,3 @@
+[testoptions]
+min_pyver=3.0
+
diff --git a/pylint/test/functional/membership_protocol_py3.txt b/pylint/test/functional/membership_protocol_py3.txt
new file mode 100644
index 0000000..4ba7575
--- /dev/null
+++ b/pylint/test/functional/membership_protocol_py3.txt
@@ -0,0 +1,3 @@
+unsupported-membership-test:34:test:Value 'IterableClass()' doesn't support membership test
+unsupported-membership-test:35:test:Value 'OldIterableClass()' doesn't support membership test
+unsupported-membership-test:36:test:Value 'ContainerClass()' doesn't support membership test