summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcpopa <devnull@localhost>2014-01-10 16:44:37 +0200
committercpopa <devnull@localhost>2014-01-10 16:44:37 +0200
commite22629f80ebbd2abf2f0843a56d5d77933ab4297 (patch)
treee96bb1f8893abccb2c81d06dda872c9a95b58b60
parentd0c4b03f255d6960f9ae4078e8fd24ff22b96d92 (diff)
parentedbb10fb6ebe63f4bcf03fbfea7393eac9fb90ed (diff)
downloadpylint-e22629f80ebbd2abf2f0843a56d5d77933ab4297.tar.gz
Merge with default.
-rw-r--r--ChangeLog10
-rw-r--r--checkers/base.py68
-rw-r--r--checkers/utils.py8
-rw-r--r--test/input/func_bad_reversed_sequence.py59
-rw-r--r--test/input/func_with_used_before_assignment.py12
-rw-r--r--test/messages/func_bad_reversed_sequence.txt9
-rw-r--r--test/messages/func_with_used_before_assignment.txt2
7 files changed, 164 insertions, 4 deletions
diff --git a/ChangeLog b/ChangeLog
index 6ed61d6..38dea0e 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,10 +2,20 @@ ChangeLog for Pylint
====================
--
+ * bitbucket #128: pylint doesn't crash when looking
+ for used-before-assignment in context manager
+ assignments.
+
+ * Add new warning, 'bad-reversed-sequence', for checking that the
+ reversed() builtin receive a sequence (implements __getitem__ and __len__,
+ without being a dict or a dict subclass) or an instance which implements
+ __reversed__.
+
* Add new warning 'bad-exception-context', checking
that `raise ... from ...` uses a proper exception context
(None or an exception).
+
2013-12-22 -- 1.1.0
* Add new check for use of deprecated pragma directives "pylint:disable-msg"
or "pylint:enable-msg" (I0022, deprecated-pragma) which was previously
diff --git a/checkers/base.py b/checkers/base.py
index d2be803..cf4304c 100644
--- a/checkers/base.py
+++ b/checkers/base.py
@@ -33,6 +33,8 @@ from pylint.checkers.utils import (
is_inside_except,
overrides_a_method,
safe_infer,
+ get_argument_from_call,
+ NoSuchArgumentError,
)
@@ -47,6 +49,8 @@ DEFAULT_NAME_RGX = re.compile('[a-z_][a-z0-9_]{2,30}$')
CLASS_ATTRIBUTE_RGX = re.compile(r'([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$')
# do not require a doc string on system methods
NO_REQUIRED_DOC_RGX = re.compile('__.*__')
+REVERSED_METHODS = (('__getitem__', '__len__'),
+ ('__reversed__', ))
BAD_FUNCTIONS = ['map', 'filter', 'apply']
if sys.version_info < (3, 0):
@@ -419,6 +423,16 @@ functions, methods
'C0121': ('Missing required attribute "%s"', # W0103
'missing-module-attribute',
'Used when an attribute required for modules is missing.'),
+
+ 'E0109': ('Missing argument to reversed()',
+ 'missing-reversed-argument',
+ 'Used when reversed() builtin didn\'t receive an argument.'),
+ 'E0111': ('The first reversed() argument is not a sequence',
+ 'bad-reversed-sequence',
+ 'Used when the first argument to reversed() builtin '
+ 'isn\'t a sequence (does not implement __reversed__, '
+ 'nor __getitem__ and __len__'),
+
}
options = (('required-attributes',
@@ -608,7 +622,9 @@ functions, methods
"""just print a warning on exec statements"""
self.add_message('exec-used', node=node)
- @check_messages('bad-builtin', 'star-args', 'exec-used')
+ @check_messages('bad-builtin', 'star-args',
+ 'exec-used', 'missing-reversed-argument',
+ 'bad-reversed-sequence')
def visit_callfunc(self, node):
"""visit a CallFunc node -> check if this is not a blacklisted builtin
call and check for * or ** use
@@ -621,6 +637,8 @@ functions, methods
name in node.root()):
if name == 'exec':
self.add_message('exec-used', node=node)
+ elif name == 'reversed':
+ self._check_reversed(node)
if name in self.config.bad_functions:
self.add_message('bad-builtin', node=node, args=name)
if node.starargs or node.kwargs:
@@ -685,7 +703,55 @@ functions, methods
return
_node = _parent
_parent = _node.parent
+
+ def _check_reversed(self, node):
+ """ check that the argument to `reversed` is a sequence """
+ try:
+ argument = safe_infer(get_argument_from_call(node, position=0))
+ except NoSuchArgumentError:
+ self.add_message('missing-reversed-argument', node=node)
+ else:
+ if argument is astroid.YES:
+ return
+ if argument is None:
+ # nothing was infered
+ # try to see if we have iter()
+ if (isinstance(node.args[0], astroid.CallFunc) and
+ node.args[0].func.name == 'iter'):
+ func = node.args[0].func.infer().next()
+ if is_builtin_object(func):
+ self.add_message('bad-reversed-sequence', node=node)
+ return
+ if isinstance(argument, astroid.Instance):
+ if (argument._proxied.name == 'dict' and
+ is_builtin_object(argument._proxied)):
+ self.add_message('bad-reversed-sequence', node=node)
+ return
+ elif any(ancestor.name == 'dict' and is_builtin_object(ancestor)
+ for ancestor in argument._proxied.ancestors()):
+ # mappings aren't accepted by reversed()
+ self.add_message('bad-reversed-sequence', node=node)
+ return
+
+ for methods in REVERSED_METHODS:
+ for meth in methods:
+ try:
+ argument.getattr(meth)
+ except astroid.NotFoundError:
+ break
+ else:
+ break
+ else:
+ # check if it is a .deque. It doesn't seem that
+ # we can retrieve special methods
+ # from C implemented constructs
+ if argument._proxied.qname().endswith(".deque"):
+ return
+ self.add_message('bad-reversed-sequence', node=node)
+ elif not isinstance(argument, (astroid.List, astroid.Tuple)):
+ # everything else is not a proper sequence for reversed()
+ self.add_message('bad-reversed-sequence', node=node)
class NameChecker(_BasicChecker):
msgs = {
diff --git a/checkers/utils.py b/checkers/utils.py
index 53b40a6..728893e 100644
--- a/checkers/utils.py
+++ b/checkers/utils.py
@@ -124,7 +124,7 @@ SPECIAL_BUILTINS = ('__builtins__',) # '__path__', '__file__')
def is_builtin_object(node):
"""Returns True if the given node is an object from the __builtin__ module."""
- return node and node.root().name == '__builtin__'
+ return node and node.root().name == BUILTINS_NAME
def is_builtin(name): # was is_native_builtin
"""return true if <name> could be considered as a builtin defined by python
@@ -154,8 +154,10 @@ def is_defined_before(var_node):
elif isinstance(_node, astroid.With):
for expr, vars in _node.items:
if expr.parent_of(var_node):
- break
- if vars and vars.name == varname:
+ break
+ if (vars and
+ isinstance(vars, astroid.AssName) and
+ vars.name == varname):
return True
elif isinstance(_node, (astroid.Lambda, astroid.Function)):
if _node.args.is_argument(varname):
diff --git a/test/input/func_bad_reversed_sequence.py b/test/input/func_bad_reversed_sequence.py
new file mode 100644
index 0000000..a394e53
--- /dev/null
+++ b/test/input/func_bad_reversed_sequence.py
@@ -0,0 +1,59 @@
+""" Checks that reversed() receive proper argument """
+
+# pylint: disable=too-few-public-methods,no-self-use
+from collections import deque
+
+__revision__ = 0
+
+class GoodReversed(object):
+ """ Implements __reversed__ """
+ def __reversed__(self):
+ return [1, 2, 3]
+
+class SecondGoodReversed(object):
+ """ Implements __len__ and __getitem__ """
+ def __len__(self):
+ return 3
+
+ def __getitem__(self, index):
+ return index
+
+class BadReversed(object):
+ """ implements only len() """
+ def __len__(self):
+ return 3
+
+class SecondBadReversed(object):
+ """ implements only __getitem__ """
+ def __getitem__(self, index):
+ return index
+
+class ThirdBadReversed(dict):
+ """ dict subclass """
+
+def uninferable(seq):
+ """ This can't be infered at this moment,
+ make sure we don't have a false positive.
+ """
+ return reversed(seq)
+
+def test():
+ """ test function """
+ seq = reversed()
+ seq = reversed(None)
+ seq = reversed([1, 2, 3])
+ seq = reversed((1, 2, 3))
+ seq = reversed(set())
+ seq = reversed({'a': 1, 'b': 2})
+ seq = reversed(iter([1, 2, 3]))
+ seq = reversed(GoodReversed())
+ seq = reversed(SecondGoodReversed())
+ seq = reversed(BadReversed())
+ seq = reversed(SecondBadReversed())
+ seq = reversed(range(100))
+ seq = reversed(ThirdBadReversed())
+ seq = reversed(lambda: None)
+ seq = reversed(deque([]))
+ seq = reversed("123")
+ seq = uninferable([1, 2, 3])
+ return seq
diff --git a/test/input/func_with_used_before_assignment.py b/test/input/func_with_used_before_assignment.py
new file mode 100644
index 0000000..c79815c
--- /dev/null
+++ b/test/input/func_with_used_before_assignment.py
@@ -0,0 +1,12 @@
+'''
+Regression test for
+https://bitbucket.org/logilab/pylint/issue/128/attributeerror-when-parsing
+'''
+from __future__ import with_statement
+__revision__ = 1
+
+def do_nothing():
+ """ empty """
+ with open("") as ctx.obj:
+ context.do()
+ context = None
diff --git a/test/messages/func_bad_reversed_sequence.txt b/test/messages/func_bad_reversed_sequence.txt
new file mode 100644
index 0000000..6e85197
--- /dev/null
+++ b/test/messages/func_bad_reversed_sequence.txt
@@ -0,0 +1,9 @@
+E: 42:test: Missing argument to reversed()
+E: 43:test: The first reversed() argument is not a sequence
+E: 46:test: The first reversed() argument is not a sequence
+E: 47:test: The first reversed() argument is not a sequence
+E: 48:test: The first reversed() argument is not a sequence
+E: 51:test: The first reversed() argument is not a sequence
+E: 52:test: The first reversed() argument is not a sequence
+E: 54:test: The first reversed() argument is not a sequence
+E: 55:test: The first reversed() argument is not a sequence \ No newline at end of file
diff --git a/test/messages/func_with_used_before_assignment.txt b/test/messages/func_with_used_before_assignment.txt
new file mode 100644
index 0000000..dc6e386
--- /dev/null
+++ b/test/messages/func_with_used_before_assignment.txt
@@ -0,0 +1,2 @@
+E: 10:do_nothing: Undefined variable 'ctx'
+E: 11:do_nothing: Using variable 'context' before assignment \ No newline at end of file