| 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, |