Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
da26b7c
Handle return outside of function at the module level
asmeurer May 27, 2015
26abc96
Catch most instances of continue and break outside a loop
asmeurer May 27, 2015
58fd813
Add tests for continue/break nested in a loop
asmeurer May 27, 2015
7dc7bd0
Fix flake8 warnings
asmeurer May 27, 2015
4b158fb
Catch continue and break in a function definition in a loop
asmeurer May 29, 2015
fc8acc7
Move some tests to the proper test function
asmeurer May 29, 2015
696f5a6
Handle continue and break nested in an for/while else clause correctly
asmeurer May 29, 2015
120bf3a
Handle 'continue' in a 'finally' block
asmeurer May 29, 2015
2a021cc
Fix flake8 errors
asmeurer May 30, 2015
e8d726a
Make the __future__ warning match the Python SyntaxError error message
asmeurer Jul 21, 2015
8fca681
Detect default 'except:' that isn't last (a SyntaxError)
asmeurer Jul 21, 2015
892596f
Fix whitespace
asmeurer Jul 21, 2015
ac7a245
Add some tests for multiple 'except:' not last instances
asmeurer Jul 21, 2015
72d3885
Fix PEP 8 warning
asmeurer Jul 21, 2015
14e81a1
Add support for non-ast syntax errors around starred assignments
asmeurer Jul 22, 2015
d844b9b
Detect yield and yield from outside a function
asmeurer Jul 22, 2015
d222013
The 'yield from' outside of function error message is the same as 'yi…
asmeurer Jul 22, 2015
590d54f
Clean up and make more efficient the CONTINUE and BREAK algorithm
asmeurer Jul 22, 2015
bc758dd
Merge branch 'master' into nonast2
asmeurer Oct 27, 2015
dc4c84f
Fix flake8 "errors"
asmeurer Oct 27, 2015
db6954f
Merge branch 'master' into nonast2
asmeurer Nov 12, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,11 +658,11 @@ def ignore(self, node):
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
Expand Down Expand Up @@ -748,8 +748,33 @@ def NAME(self, node):
# 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be broken up into functions so it can be more clear what the overall algorithm is?

Flat is better than nested.

-- from The Zen of Python, by Tim Peters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this (I told you I would get stalled). It's not clear to me how to break this up. Would it be clearer if it were written recursively instead of using a while loop? I can definitely add more comments explaining the algorithm and what it's checking for.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than comments, I'd look for loops which could be broken out into specific functions. Then the name of the functions can serve as the documentation.

The high-level algorithm here, as I understand it, is "when a continue or break is encountered, it's an error if that's not in a loop". So I might expect to find a function named isInLoop(). Then maybe CONTINUE() might be something like:

def CONTINUE(self, node):
    if self.isInLoop(node):
        return
    if isinstance(node, ast.Continue):
        self.report(messages.ContinueOutsideLoop, node)
     else:  # ast.Break
        self.report(messages.BreakOutsideLoop, node)

Also, the operation, "traverse the parents until finding a node matching some predicate" has become a pretty common operation, so that can be a function too. Just grepping for parent I find a few, the poorly named getParent, and also on_conditional_branch().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's roughly the algorithm (and more or less what I had in the first pass), but not completely correct. More accurate is "continue or break is only valid if it is in a loop, and if it a function or class definition, the loop must also be inside the function or class definition. And also for continue if it is in a finally block the loop must also be inside the finally block." I'm not sure if that can be written as a simple predicate (but I may just need to think about it harder).

I guess the algorithm itself is not too complicated (it's just, walk up the tree and note what you see first, a loop, a function definition, a class definition, or (for continue) a finally block). The complications are arising from the fact that for loops and finally blocks, the nodes have different subtrees, so just saying "the continue is in the ast.For is not enough because it could be in the orelse part. So maybe that's what really should be abstracted here.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also occurs to me that this algorithm is a rather inefficient way of dealing with this issue. When I see a loop, I don't know if I am in the loop or the orelse part, so I walk the whole tree to make sure I'm not in the orelse part. But I really ought to be keeping track of the previous node and checking that way. Fixing this may also make the whole algorithm clearer (and I also probably should comment the code a little more). Let me give it a try and you can see what you think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, the latest version should be much easier to read. I don't think it's necessary to split it into a function at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus the algorithm itself no longer uses the for/else construct that it was so concerned about, which is a plus, as few people understand how to read that correctly.


BREAK = CONTINUE

def RETURN(self, node):
if isinstance(self.scope, ClassScope):
if isinstance(self.scope, (ClassScope, ModuleScope)):
self.report(messages.ReturnOutsideFunction, node)
return

Expand All @@ -762,6 +787,10 @@ def RETURN(self, node):
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)

Expand Down Expand Up @@ -886,6 +915,31 @@ def AUGASSIGN(self, node):
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
Expand Down Expand Up @@ -914,12 +968,15 @@ def IMPORTFROM(self, node):
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:
Expand Down
55 changes: 53 additions & 2 deletions pyflakes/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ def __init__(self, filename, loc, name):


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):
Expand All @@ -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'
Loading