summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAaron Meurer <asmeurer@gmail.com>2015-11-13 07:31:40 -0500
committerPhil Frost <indigo@bitglue.com>2015-11-13 07:33:34 -0500
commit7f751bec39459c9799f0d0553589e6193c1c00a4 (patch)
treebe4a5e60496cf2699275d44e7db580d115ca51c8
parent93aa3c435505b8541b151c3e4b24c0ec4333f0bb (diff)
downloadpyflakes-nonast.tar.gz
Check for non-ast SyntaxErrorsnonast
This includes return and yield outside of a function and break and continue outside of a loop. Fixes lp 1293654. The problem is that these SyntaxErrors are not encoded in the ast grammar, so they are not detected when just compiling to ast. You must compile down to bytecode to catch them. The advantage here is that we can still check for other kinds of errors in this case, because the ast is still valid.
-rw-r--r--pyflakes/checker.py65
-rw-r--r--pyflakes/messages.py55
-rw-r--r--pyflakes/test/test_other.py662
3 files changed, 772 insertions, 10 deletions
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 7a51328..3b35ca6 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -658,11 +658,11 @@ class Checker(object):
ASYNCWITH = ASYNCWITHITEM = RAISE = TRYFINALLY = ASSERT = EXEC = \
EXPR = ASSIGN = handleChildren
- CONTINUE = BREAK = PASS = ignore
+ PASS = ignore
# "expr" type nodes
BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = \
- COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = LIST = TUPLE = \
+ COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = \
STARRED = NAMECONSTANT = handleChildren
NUM = STR = BYTES = ELLIPSIS = ignore
@@ -748,8 +748,33 @@ class Checker(object):
# arguments, but these aren't dispatched through here
raise RuntimeError("Got impossible expression context: %r" % (node.ctx,))
+ def CONTINUE(self, node):
+ # Walk the tree up until we see a loop (OK), a function or class
+ # definition (not OK), for 'continue', a finally block (not OK), or
+ # the top module scope (not OK)
+ n = node
+ while hasattr(n, 'parent'):
+ n, n_child = n.parent, n
+ if isinstance(n, (ast.While, ast.For)):
+ # Doesn't apply unless it's in the loop itself
+ if n_child not in n.orelse:
+ return
+ if isinstance(n, (ast.FunctionDef, ast.ClassDef)):
+ break
+ # Handle Try/TryFinally difference in Python < and >= 3.3
+ if hasattr(n, 'finalbody') and isinstance(node, ast.Continue):
+ if n_child in n.finalbody:
+ self.report(messages.ContinueInFinally, node)
+ return
+ if isinstance(node, ast.Continue):
+ self.report(messages.ContinueOutsideLoop, node)
+ else: # ast.Break
+ self.report(messages.BreakOutsideLoop, node)
+
+ BREAK = CONTINUE
+
def RETURN(self, node):
- if isinstance(self.scope, ClassScope):
+ if isinstance(self.scope, (ClassScope, ModuleScope)):
self.report(messages.ReturnOutsideFunction, node)
return
@@ -762,6 +787,10 @@ class Checker(object):
self.handleNode(node.value, node)
def YIELD(self, node):
+ if isinstance(self.scope, (ClassScope, ModuleScope)):
+ self.report(messages.YieldOutsideFunction, node)
+ return
+
self.scope.isGenerator = True
self.handleNode(node.value, node)
@@ -886,6 +915,31 @@ class Checker(object):
self.handleNode(node.value, node)
self.handleNode(node.target, node)
+ def TUPLE(self, node):
+ if not PY2 and isinstance(node.ctx, ast.Store):
+ # Python 3 advanced tuple unpacking: a, *b, c = d.
+ # Only one starred expression is allowed, and no more than 1<<8
+ # assignments are allowed before a stared expression. There is
+ # also a limit of 1<<24 expressions after the starred expression,
+ # which is impossible to test due to memory restrictions, but we
+ # add it here anyway
+ has_starred = False
+ star_loc = -1
+ for i, n in enumerate(node.elts):
+ if isinstance(n, ast.Starred):
+ if has_starred:
+ self.report(messages.TwoStarredExpressions, node)
+ # The SyntaxError doesn't distinguish two from more
+ # than two.
+ break
+ has_starred = True
+ star_loc = i
+ if star_loc >= 1 << 8 or len(node.elts) - star_loc - 1 >= 1 << 24:
+ self.report(messages.TooManyExpressionsInStarredAssignment, node)
+ self.handleChildren(node)
+
+ LIST = TUPLE
+
def IMPORT(self, node):
for alias in node.names:
name = alias.asname or alias.name
@@ -914,12 +968,15 @@ class Checker(object):
def TRY(self, node):
handler_names = []
# List the exception handlers
- for handler in node.handlers:
+ for i, handler in enumerate(node.handlers):
if isinstance(handler.type, ast.Tuple):
for exc_type in handler.type.elts:
handler_names.append(getNodeName(exc_type))
elif handler.type:
handler_names.append(getNodeName(handler.type))
+
+ if handler.type is None and i < len(node.handlers) - 1:
+ self.report(messages.DefaultExceptNotLast, handler)
# Memorize the except handlers and process the body
self.exceptHandlers.append(handler_names)
for child in node.body:
diff --git a/pyflakes/messages.py b/pyflakes/messages.py
index 40e0cfe..27167e0 100644
--- a/pyflakes/messages.py
+++ b/pyflakes/messages.py
@@ -101,11 +101,11 @@ class DuplicateArgument(Message):
class LateFutureImport(Message):
- message = 'future import(s) %r after other statements'
+ message = 'from __future__ imports must occur at the beginning of the file'
def __init__(self, filename, loc, names):
Message.__init__(self, filename, loc)
- self.message_args = (names,)
+ self.message_args = ()
class UnusedVariable(Message):
@@ -132,3 +132,54 @@ class ReturnOutsideFunction(Message):
Indicates a return statement outside of a function/method.
"""
message = '\'return\' outside function'
+
+
+class YieldOutsideFunction(Message):
+ """
+ Indicates a yield or yield from statement outside of a function/method.
+ """
+ message = '\'yield\' outside function'
+
+
+# For whatever reason, Python gives different error messages for these two. We
+# match the Python error message exactly.
+class ContinueOutsideLoop(Message):
+ """
+ Indicates a continue statement outside of a while or for loop.
+ """
+ message = '\'continue\' not properly in loop'
+
+
+class BreakOutsideLoop(Message):
+ """
+ Indicates a break statement outside of a while or for loop.
+ """
+ message = '\'break\' outside loop'
+
+
+class ContinueInFinally(Message):
+ """
+ Indicates a continue statement in a finally block in a while or for loop.
+ """
+ message = '\'continue\' not supported inside \'finally\' clause'
+
+
+class DefaultExceptNotLast(Message):
+ """
+ Indicates an except: block as not the last exception handler.
+ """
+ message = 'default \'except:\' must be last'
+
+
+class TwoStarredExpressions(Message):
+ """
+ Two or more starred expressions in an assignment (a, *b, *c = d).
+ """
+ message = 'two starred expressions in assignment'
+
+
+class TooManyExpressionsInStarredAssignment(Message):
+ """
+ Too many expressions in an assignment with star-unpacking
+ """
+ message = 'too many expressions in star-unpacking assignment'
diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index 3167a1d..1891c15 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -353,6 +353,664 @@ class Test(TestCase):
return
''', m.ReturnOutsideFunction)
+ def test_moduleWithReturn(self):
+ """
+ If a return is used at the module level, a warning is emitted.
+ """
+ self.flakes('''
+ return
+ ''', m.ReturnOutsideFunction)
+
+ def test_classWithYield(self):
+ """
+ If a yield is used inside a class, a warning is emitted.
+ """
+ self.flakes('''
+ class Foo(object):
+ yield
+ ''', m.YieldOutsideFunction)
+
+ def test_moduleWithYield(self):
+ """
+ If a yield is used at the module level, a warning is emitted.
+ """
+ self.flakes('''
+ yield
+ ''', m.YieldOutsideFunction)
+
+ @skipIf(version_info < (3, 3), "Python >= 3.3 only")
+ def test_classWithYieldFrom(self):
+ """
+ If a yield from is used inside a class, a warning is emitted.
+ """
+ self.flakes('''
+ class Foo(object):
+ yield from range(10)
+ ''', m.YieldOutsideFunction)
+
+ @skipIf(version_info < (3, 3), "Python >= 3.3 only")
+ def test_moduleWithYieldFrom(self):
+ """
+ If a yield from is used at the module level, a warning is emitted.
+ """
+ self.flakes('''
+ yield from range(10)
+ ''', m.YieldOutsideFunction)
+
+ def test_continueOutsideLoop(self):
+ self.flakes('''
+ continue
+ ''', m.ContinueOutsideLoop)
+
+ self.flakes('''
+ def f():
+ continue
+ ''', m.ContinueOutsideLoop)
+
+ self.flakes('''
+ while True:
+ pass
+ else:
+ continue
+ ''', m.ContinueOutsideLoop)
+
+ self.flakes('''
+ while True:
+ pass
+ else:
+ if 1:
+ if 2:
+ continue
+ ''', m.ContinueOutsideLoop)
+
+ self.flakes('''
+ while True:
+ def f():
+ continue
+ ''', m.ContinueOutsideLoop)
+
+ self.flakes('''
+ while True:
+ class A:
+ continue
+ ''', m.ContinueOutsideLoop)
+
+ def test_continueInsideLoop(self):
+ self.flakes('''
+ while True:
+ continue
+ ''')
+
+ self.flakes('''
+ for i in range(10):
+ continue
+ ''')
+
+ self.flakes('''
+ while True:
+ if 1:
+ continue
+ ''')
+
+ self.flakes('''
+ for i in range(10):
+ if 1:
+ continue
+ ''')
+
+ self.flakes('''
+ while True:
+ while True:
+ pass
+ else:
+ continue
+ else:
+ pass
+ ''')
+
+ self.flakes('''
+ while True:
+ try:
+ pass
+ finally:
+ while True:
+ continue
+ ''')
+
+ def test_continueInFinally(self):
+ # 'continue' inside 'finally' is a special syntax error
+ self.flakes('''
+ while True:
+ try:
+ pass
+ finally:
+ continue
+ ''', m.ContinueInFinally)
+
+ self.flakes('''
+ while True:
+ try:
+ pass
+ finally:
+ if 1:
+ if 2:
+ continue
+ ''', m.ContinueInFinally)
+
+ # Even when not in a loop, this is the error Python gives
+ self.flakes('''
+ try:
+ pass
+ finally:
+ continue
+ ''', m.ContinueInFinally)
+
+ def test_breakOutsideLoop(self):
+ self.flakes('''
+ break
+ ''', m.BreakOutsideLoop)
+
+ self.flakes('''
+ def f():
+ break
+ ''', m.BreakOutsideLoop)
+
+ self.flakes('''
+ while True:
+ pass
+ else:
+ break
+ ''', m.BreakOutsideLoop)
+
+ self.flakes('''
+ while True:
+ pass
+ else:
+ if 1:
+ if 2:
+ break
+ ''', m.BreakOutsideLoop)
+
+ self.flakes('''
+ while True:
+ def f():
+ break
+ ''', m.BreakOutsideLoop)
+
+ self.flakes('''
+ while True:
+ class A:
+ break
+ ''', m.BreakOutsideLoop)
+
+ self.flakes('''
+ try:
+ pass
+ finally:
+ break
+ ''', m.BreakOutsideLoop)
+
+ def test_breakInsideLoop(self):
+ self.flakes('''
+ while True:
+ break
+ ''')
+
+ self.flakes('''
+ for i in range(10):
+ break
+ ''')
+
+ self.flakes('''
+ while True:
+ if 1:
+ break
+ ''')
+
+ self.flakes('''
+ for i in range(10):
+ if 1:
+ break
+ ''')
+
+ self.flakes('''
+ while True:
+ while True:
+ pass
+ else:
+ break
+ else:
+ pass
+ ''')
+
+ self.flakes('''
+ while True:
+ try:
+ pass
+ finally:
+ while True:
+ break
+ ''')
+
+ self.flakes('''
+ while True:
+ try:
+ pass
+ finally:
+ break
+ ''')
+
+ self.flakes('''
+ while True:
+ try:
+ pass
+ finally:
+ if 1:
+ if 2:
+ break
+ ''')
+
+ def test_defaultExceptLast(self):
+ """
+ A default except block should be last.
+
+ YES:
+
+ try:
+ ...
+ except Exception:
+ ...
+ except:
+ ...
+
+ NO:
+
+ try:
+ ...
+ except:
+ ...
+ except Exception:
+ ...
+ """
+ self.flakes('''
+ try:
+ pass
+ except ValueError:
+ pass
+ ''')
+
+ self.flakes('''
+ try:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ ''')
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ ''')
+
+ self.flakes('''
+ try:
+ pass
+ except ValueError:
+ pass
+ else:
+ pass
+ ''')
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ else:
+ pass
+ ''')
+
+ self.flakes('''
+ try:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ else:
+ pass
+ ''')
+
+ def test_defaultExceptNotLast(self):
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ ''', m.DefaultExceptNotLast, m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ else:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except:
+ pass
+ else:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ else:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ else:
+ pass
+ ''', m.DefaultExceptNotLast, m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast, m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ else:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except:
+ pass
+ else:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ else:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast)
+
+ self.flakes('''
+ try:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ except:
+ pass
+ except ValueError:
+ pass
+ else:
+ pass
+ finally:
+ pass
+ ''', m.DefaultExceptNotLast, m.DefaultExceptNotLast)
+
+ @skipIf(version_info < (3,), "Python 3 only")
+ def test_starredAssignmentNoError(self):
+ """
+ Python 3 extended iterable unpacking
+ """
+ self.flakes('''
+ a, *b = range(10)
+ ''')
+
+ self.flakes('''
+ *a, b = range(10)
+ ''')
+
+ self.flakes('''
+ a, *b, c = range(10)
+ ''')
+
+ self.flakes('''
+ (a, *b) = range(10)
+ ''')
+
+ self.flakes('''
+ (*a, b) = range(10)
+ ''')
+
+ self.flakes('''
+ (a, *b, c) = range(10)
+ ''')
+
+ self.flakes('''
+ [a, *b] = range(10)
+ ''')
+
+ self.flakes('''
+ [*a, b] = range(10)
+ ''')
+
+ self.flakes('''
+ [a, *b, c] = range(10)
+ ''')
+
+ # Taken from test_unpack_ex.py in the cPython source
+ s = ", ".join("a%d" % i for i in range(1 << 8 - 1)) + \
+ ", *rest = range(1<<8)"
+ self.flakes(s)
+
+ s = "(" + ", ".join("a%d" % i for i in range(1 << 8 - 1)) + \
+ ", *rest) = range(1<<8)"
+ self.flakes(s)
+
+ s = "[" + ", ".join("a%d" % i for i in range(1 << 8 - 1)) + \
+ ", *rest] = range(1<<8)"
+ self.flakes(s)
+
+ @skipIf(version_info < (3, ), "Python 3 only")
+ def test_starredAssignmentErrors(self):
+ """
+ SyntaxErrors (not encoded in the ast) surrounding Python 3 extended
+ iterable unpacking
+ """
+ # Taken from test_unpack_ex.py in the cPython source
+ s = ", ".join("a%d" % i for i in range(1 << 8)) + \
+ ", *rest = range(1<<8 + 1)"
+ self.flakes(s, m.TooManyExpressionsInStarredAssignment)
+
+ s = "(" + ", ".join("a%d" % i for i in range(1 << 8)) + \
+ ", *rest) = range(1<<8 + 1)"
+ self.flakes(s, m.TooManyExpressionsInStarredAssignment)
+
+ s = "[" + ", ".join("a%d" % i for i in range(1 << 8)) + \
+ ", *rest] = range(1<<8 + 1)"
+ self.flakes(s, m.TooManyExpressionsInStarredAssignment)
+
+ s = ", ".join("a%d" % i for i in range(1 << 8 + 1)) + \
+ ", *rest = range(1<<8 + 2)"
+ self.flakes(s, m.TooManyExpressionsInStarredAssignment)
+
+ s = "(" + ", ".join("a%d" % i for i in range(1 << 8 + 1)) + \
+ ", *rest) = range(1<<8 + 2)"
+ self.flakes(s, m.TooManyExpressionsInStarredAssignment)
+
+ s = "[" + ", ".join("a%d" % i for i in range(1 << 8 + 1)) + \
+ ", *rest] = range(1<<8 + 2)"
+ self.flakes(s, m.TooManyExpressionsInStarredAssignment)
+
+ # No way we can actually test this!
+ # s = "*rest, " + ", ".join("a%d" % i for i in range(1<<24)) + \
+ # ", *rest = range(1<<24 + 1)"
+ # self.flakes(s, m.TooManyExpressionsInStarredAssignment)
+
+ self.flakes('''
+ a, *b, *c = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ a, *b, c, *d = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ *a, *b, *c = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ (a, *b, *c) = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ (a, *b, c, *d) = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ (*a, *b, *c) = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ [a, *b, *c] = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ [a, *b, c, *d] = range(10)
+ ''', m.TwoStarredExpressions)
+
+ self.flakes('''
+ [*a, *b, *c] = range(10)
+ ''', m.TwoStarredExpressions)
+
@skip("todo: Too hard to make this warn but other cases stay silent")
def test_doubleAssignment(self):
"""
@@ -985,10 +1643,6 @@ class TestUnusedAssignment(TestCase):
yield from foo()
''', m.UndefinedName)
- def test_returnOnly(self):
- """Do not crash on lone "return"."""
- self.flakes('return 2')
-
@skipIf(version_info < (3, 6), 'new in Python 3.6')
def test_f_string(self):
"""Test PEP 498 f-strings are treated as a usage."""