Index: flake8-eyeo/flake8_eyeo.py |
=================================================================== |
--- a/flake8-eyeo/flake8_eyeo.py |
+++ b/flake8-eyeo/flake8_eyeo.py |
@@ -68,6 +68,13 @@ |
return type(node).__name__.lower() |
+def get_descendant_nodes(node): |
+ for child in ast.iter_child_nodes(node): |
+ yield (child, node) |
+ for nodes in get_descendant_nodes(child): |
+ yield nodes |
+ |
+ |
class TreeVisitor(ast.NodeVisitor): |
Scope = collections.namedtuple('Scope', ['node', 'names', 'globals']) |
@@ -424,55 +431,67 @@ |
first_token = False |
-def check_redundant_parenthesis(logical_line, tokens): |
- start_line = tokens[0][2][0] |
- level = 0 |
- statement = None |
+def check_redundant_parenthesis(tree, lines, file_tokens): |
+ orig = ast.dump(tree) |
+ nodes = get_descendant_nodes(tree) |
+ stack = [] |
- for i, (kind, token, _, end, _) in enumerate(tokens): |
- if kind == tokenize.INDENT or kind == tokenize.DEDENT: |
+ for i, (kind, token, _, _, _) in enumerate(file_tokens): |
+ if kind != tokenize.OP: |
continue |
- if statement is None: |
- # logical line doesn't start with an if, elif or while statement |
- if kind != tokenize.NAME or token not in {'if', 'elif', 'while'}: |
- break |
+ if token == '(': |
+ stack.append(i) |
+ elif token == ')': |
+ start = stack.pop() |
+ sample = lines[:] |
- # expression doesn't start with parenthesis |
- next_token = tokens[i + 1] |
- if next_token[:2] != (tokenize.OP, '('): |
- break |
+ for pos in [i, start]: |
+ _, _, (lineno, col1), (_, col2), _ = file_tokens[pos] |
+ lineno -= 1 |
+ sample[lineno] = (sample[lineno][:col1] + |
+ sample[lineno][col2:]) |
- # expression is empty tuple |
- if tokens[i + 2][:2] == (tokenize.OP, ')'): |
- break |
+ try: |
+ modified = ast.parse(''.join(sample)) |
+ except SyntaxError: |
+ # Parentheses syntactically required. |
+ continue |
- statement = token |
- pos = next_token[2] |
- continue |
+ # Parentheses logically required. |
+ if orig != ast.dump(modified): |
+ continue |
- # expression ends on a different line, parenthesis are necessary |
- if end[0] > start_line: |
- break |
+ pos = file_tokens[start][2] |
+ while True: |
+ node, parent = next(nodes) |
+ if pos < (getattr(node, 'lineno', -1), |
+ getattr(node, 'col_offset', -1)): |
+ break |
- if kind == tokenize.OP: |
- if token == ',': |
- # expression is non-empty tuple |
- if level == 1: |
- break |
- elif token == '(': |
- level += 1 |
- elif token == ')': |
- level -= 1 |
- if level == 0: |
- # outer parenthesis closed before end of expression |
- if tokens[i + 1][:2] != (tokenize.OP, ':'): |
- break |
+ # Allow redundant parentheses for readability, |
+ # when creating tuples (but not when unpacking variables), |
+ # nested operations and comparisons inside assignments. |
+ is_tuple = ( |
+ isinstance(node, ast.Tuple) and not ( |
+ isinstance(parent, (ast.For, ast.comprehension)) and |
+ node == parent.target or |
+ isinstance(parent, ast.Assign) and |
+ node in parent.targets |
+ ) |
+ ) |
+ is_nested_op = ( |
+ isinstance(node, (ast.BinOp, ast.BoolOp)) and |
+ isinstance(parent, (ast.BinOp, ast.BoolOp)) |
+ ) |
+ is_compare_in_assign = ( |
+ isinstance(parent, (ast.Assign, ast.keyword)) and |
+ any(isinstance(x, ast.Compare) for x in ast.walk(node)) |
+ ) |
+ if is_tuple or is_nested_op or is_compare_in_assign: |
+ continue |
- return [(pos, 'A111 redundant parenthesis for {} ' |
- 'statement'.format(statement))] |
- |
- return [] |
+ yield (pos[0], pos[1], 'A111 redundant parenthesis', None) |
for checker in [check_ast, check_non_default_encoding, |