| Left: | ||
| Right: |
| LEFT | RIGHT |
|---|---|
| 1 # This file is part of Adblock Plus <https://adblockplus.org/>, | 1 # This file is part of Adblock Plus <https://adblockplus.org/>, |
| 2 # Copyright (C) 2006-2016 Eyeo GmbH | 2 # Copyright (C) 2006-2016 Eyeo GmbH |
| 3 # | 3 # |
| 4 # Adblock Plus is free software: you can redistribute it and/or modify | 4 # Adblock Plus is free software: you can redistribute it and/or modify |
| 5 # it under the terms of the GNU General Public License version 3 as | 5 # it under the terms of the GNU General Public License version 3 as |
| 6 # published by the Free Software Foundation. | 6 # published by the Free Software Foundation. |
| 7 # | 7 # |
| 8 # Adblock Plus is distributed in the hope that it will be useful, | 8 # Adblock Plus is distributed in the hope that it will be useful, |
| 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of | 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 # GNU General Public License for more details. | 11 # GNU General Public License for more details. |
| 12 # | 12 # |
| 13 # You should have received a copy of the GNU General Public License | 13 # You should have received a copy of the GNU General Public License |
| 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
| 15 | 15 |
| 16 import ast | 16 import ast |
| 17 import re | 17 import re |
| 18 import tokenize | 18 import tokenize |
| 19 import sys | 19 import sys |
| 20 import collections | 20 import collections |
| 21 | 21 |
| 22 try: | 22 try: |
| 23 import builtins | 23 import builtins |
| 24 except ImportError: | 24 except ImportError: |
| 25 import __builtin__ as builtins | 25 import __builtin__ as builtins |
| 26 | 26 |
| 27 import pkg_resources | 27 import pkg_resources |
| 28 | 28 |
| 29 try: | |
| 30 ascii | |
| 31 except NameError: | |
| 32 ascii = repr | |
| 33 | |
| 29 __version__ = pkg_resources.get_distribution('flake8-abp').version | 34 __version__ = pkg_resources.get_distribution('flake8-abp').version |
| 30 | 35 |
| 31 DISCOURAGED_APIS = { | 36 DISCOURAGED_APIS = { |
| 32 're.match': 're.search', | 37 're.match': 're.search', |
| 33 'codecs.open': 'io.open', | 38 'codecs.open': 'io.open', |
| 34 } | 39 } |
| 35 | 40 |
| 36 ESSENTIAL_BUILTINS = set(dir(builtins)) - {'apply', 'basestring', 'buffer', | 41 ESSENTIAL_BUILTINS = set(dir(builtins)) - {'apply', 'buffer', 'coerce', |
|
Vasily Kuznetsov
2016/05/09 13:39:51
What were the criteria for deciding what is an ess
Sebastian Noack
2016/05/09 16:49:11
The names explicitly excluded were the builtins th
Vasily Kuznetsov
2016/05/09 17:37:32
Yep, makes sense. It would be good to record it so
Sebastian Noack
2016/05/11 10:50:09
I think the general rule for the style guide shoul
Vasily Kuznetsov
2016/05/11 14:09:17
Ok, I think saying that builtins should be overrid
| |
| 37 'cmp', 'coerce', 'execfile', | 42 'intern', 'file'} |
| 38 'file', 'intern', 'long', | |
| 39 'raw_input', 'reduce', 'reload', | |
| 40 'unichr', 'unicode', 'xrange'} | |
| 41 | 43 |
| 42 LEAVE_BLOCK = (ast.Return, ast.Raise, ast.Continue, ast.Break) | 44 LEAVE_BLOCK = (ast.Return, ast.Raise, ast.Continue, ast.Break) |
| 43 VOLATILE = object() | 45 VOLATILE = object() |
| 44 | 46 |
| 45 | 47 |
| 46 def evaluate(node): | 48 def evaluate(node): |
| 47 try: | 49 try: |
| 48 return eval(compile(ast.Expression(node), '', 'eval'), {}) | 50 return eval(compile(ast.Expression(node), '', 'eval'), {}) |
| 49 except Exception: | 51 except Exception: |
|
Vasily Kuznetsov
2016/05/09 13:39:51
As I understand this exception would be thrown whe
Sebastian Noack
2016/05/09 16:49:11
Yes, it can be one of a wide range (if not any) ex
Vasily Kuznetsov
2016/05/09 17:37:32
I see. I just got a bit confused by this being cal
| |
| 50 return VOLATILE | 52 return VOLATILE |
| 51 | 53 |
| 52 | 54 |
| 53 def is_const(node): | 55 def is_const(node): |
| 54 return evaluate(node) is not VOLATILE | 56 return evaluate(node) is not VOLATILE |
| 55 | 57 |
| 56 | 58 |
| 57 def get_identifier(node): | 59 def get_identifier(node): |
| 58 if isinstance(node, ast.Name): | 60 if isinstance(node, ast.Name): |
| 59 return node.id | 61 return node.id |
| 60 if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name): | 62 if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name): |
| 61 return '{}.{}'.format(node.value.id, node.attr) | 63 return '{}.{}'.format(node.value.id, node.attr) |
| 62 | 64 |
| 63 | 65 |
| 64 def get_statement(node): | 66 def get_statement(node): |
| 65 return type(node).__name__.lower() | 67 return type(node).__name__.lower() |
| 66 | 68 |
| 67 | 69 |
| 68 class TreeVisitor(ast.NodeVisitor): | 70 class TreeVisitor(ast.NodeVisitor): |
| 69 Scope = collections.namedtuple('Scope', ['node', 'names', 'globals']) | 71 Scope = collections.namedtuple('Scope', ['node', 'names', 'globals']) |
| 70 | 72 |
| 71 def __init__(self): | 73 def __init__(self): |
| 72 self.errors = [] | 74 self.errors = [] |
| 73 self.scope_stack = [] | 75 self.scope_stack = [] |
| 74 | 76 |
| 75 def _visit_block(self, nodes, block_required=False, | 77 def _visit_block(self, nodes, block_required=False, |
| 76 nodes_required=True, docstring=False): | 78 nodes_required=True, docstring=False, |
| 79 can_have_unused_expr=False): | |
| 77 pass_node = None | 80 pass_node = None |
| 78 has_non_pass = False | 81 has_non_pass = False |
| 79 leave_node = None | 82 leave_node = None |
| 80 dead_code = False | 83 dead_code = False |
| 81 | 84 |
| 82 for i, node in enumerate(nodes): | 85 for i, node in enumerate(nodes): |
| 83 if isinstance(node, ast.Pass): | 86 if isinstance(node, ast.Pass): |
| 84 pass_node = node | 87 pass_node = node |
| 85 else: | 88 else: |
| 86 has_non_pass = True | 89 has_non_pass = True |
| 87 | 90 |
| 88 if leave_node and not dead_code: | 91 if leave_node and not dead_code: |
| 89 dead_code = True | 92 dead_code = True |
| 90 statement = get_statement(leave_node) | 93 statement = get_statement(leave_node) |
| 91 self.errors.append((node, 'A202 dead code after ' | 94 self.errors.append((node, 'A202 dead code after ' |
| 92 '{}'.format(statement))) | 95 '{}'.format(statement))) |
| 93 | 96 |
| 94 if isinstance(node, LEAVE_BLOCK): | 97 if isinstance(node, LEAVE_BLOCK): |
| 95 leave_node = node | 98 leave_node = node |
| 96 | 99 |
| 97 if not isinstance(node, ast.Expr): | 100 if can_have_unused_expr or not isinstance(node, ast.Expr): |
| 98 continue | 101 continue |
| 99 if docstring and i == 0 and isinstance(node.value, ast.Str): | 102 if docstring and i == 0 and isinstance(node.value, ast.Str): |
| 100 continue | 103 continue |
| 101 if isinstance(node.value, (ast.Call, ast.Yield)): | 104 if isinstance(node.value, (ast.Call, ast.Yield)): |
| 102 continue | 105 continue |
| 103 | 106 |
| 104 self.errors.append((node, 'A203 unused expression')) | 107 self.errors.append((node, 'A203 unused expression')) |
| 105 | 108 |
| 106 if pass_node: | 109 if pass_node: |
| 107 if not nodes_required or len(nodes) > 1: | 110 if not nodes_required or len(nodes) > 1: |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 118 for handler in handlers: | 121 for handler in handlers: |
| 119 for child in handler.body: | 122 for child in handler.body: |
| 120 if isinstance(child, LEAVE_BLOCK): | 123 if isinstance(child, LEAVE_BLOCK): |
| 121 leave_node = child | 124 leave_node = child |
| 122 break | 125 break |
| 123 else: | 126 else: |
| 124 return | 127 return |
| 125 | 128 |
| 126 statement = get_statement(leave_node) | 129 statement = get_statement(leave_node) |
| 127 self.errors.append((node.orelse[0], | 130 self.errors.append((node.orelse[0], |
| 128 'A206 redundant else statement after {} ' | 131 'A206 Extraneous else statement after {} ' |
| 129 'in {}-clause'.format(statement, clause))) | 132 'in {}-clause'.format(statement, clause))) |
| 130 | 133 |
| 131 def visit_If(self, node): | 134 def visit_If(self, node): |
| 132 self._visit_block(node.body, block_required=bool(node.orelse)) | 135 self._visit_block(node.body, block_required=bool(node.orelse)) |
| 133 self._visit_block(node.orelse) | 136 self._visit_block(node.orelse) |
| 134 self._check_redundant_else(node, [node], 'if') | 137 self._check_redundant_else(node, [node], 'if') |
| 135 self.generic_visit(node) | 138 self.generic_visit(node) |
| 136 | 139 |
| 137 def visit_Try(self, node): | 140 def visit_Try(self, node): |
| 138 self._visit_block(node.body) | 141 self._visit_block(node.body, can_have_unused_expr=bool(node.handlers)) |
| 139 self._visit_block(node.orelse) | 142 self._visit_block(node.orelse) |
| 140 self._visit_block(node.finalbody) | 143 self._visit_block(node.finalbody) |
| 141 self._check_redundant_else(node, node.handlers, 'except') | 144 self._check_redundant_else(node, node.handlers, 'except') |
| 142 self.generic_visit(node) | 145 self.generic_visit(node) |
| 143 | 146 |
| 144 def visit_TryExcept(self, node): | 147 def visit_TryExcept(self, node): |
| 145 self._visit_block(node.body) | 148 self._visit_block(node.body, can_have_unused_expr=True) |
| 146 self._visit_block(node.orelse) | 149 self._visit_block(node.orelse) |
| 147 self._check_redundant_else(node, node.handlers, 'except') | 150 self._check_redundant_else(node, node.handlers, 'except') |
| 148 self.generic_visit(node) | 151 self.generic_visit(node) |
| 149 | 152 |
| 150 def visit_TryFinally(self, node): | 153 def visit_TryFinally(self, node): |
| 151 self._visit_block(node.body) | 154 self._visit_block(node.body) |
| 152 self._visit_block(node.finalbody) | 155 self._visit_block(node.finalbody) |
| 153 self.generic_visit(node) | 156 self.generic_visit(node) |
| 154 | 157 |
| 155 def visit_ExceptHandler(self, node): | 158 def visit_ExceptHandler(self, node): |
| 156 self._visit_block(node.body, block_required=True) | 159 self._visit_block(node.body, block_required=True) |
| 157 self.generic_visit(node) | 160 self.generic_visit(node) |
| 158 | 161 |
| 159 def _visit_stored_name(self, node, name): | 162 def _visit_stored_name(self, node, name): |
| 160 scope = self.scope_stack[-1] | 163 scope = self.scope_stack[-1] |
| 161 scope.names.add(name) | 164 scope.names.add(name) |
| 162 | 165 |
| 163 if name in ESSENTIAL_BUILTINS and not isinstance(scope.node, | 166 if name in ESSENTIAL_BUILTINS and isinstance(scope.node, |
| 164 ast.ClassDef): | 167 ast.FunctionDef): |
| 165 self.errors.append((node, 'A302 redefined built-in ' + name)) | 168 self.errors.append((node, 'A302 redefined built-in ' + name)) |
| 166 | 169 |
| 167 def visit_Name(self, node): | 170 def visit_Name(self, node): |
| 168 if isinstance(node.ctx, ast.Store): | 171 if isinstance(node.ctx, (ast.Store, ast.Param)): |
| 169 self._visit_stored_name(node, node.id) | 172 self._visit_stored_name(node, node.id) |
| 173 | |
| 174 def visit_arg(self, node): | |
| 175 self._visit_stored_name(node, node.arg) | |
| 170 | 176 |
| 171 def _visit_with_scope(self, node): | 177 def _visit_with_scope(self, node): |
| 172 scope = self.Scope(node, names=set(), globals=[]) | 178 scope = self.Scope(node, names=set(), globals=[]) |
| 173 self.scope_stack.append(scope) | 179 self.scope_stack.append(scope) |
| 174 self.generic_visit(node) | 180 self.generic_visit(node) |
| 175 del self.scope_stack[-1] | 181 del self.scope_stack[-1] |
| 176 return scope | 182 return scope |
| 177 | 183 |
| 178 def visit_Module(self, node): | 184 def visit_Module(self, node): |
| 179 self._visit_block(node.body, block_required=True, | 185 self._visit_block(node.body, block_required=True, |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 236 '% operator for string formatting')) | 242 '% operator for string formatting')) |
| 237 | 243 |
| 238 multi_addition = (isinstance(node.op, ast.Add) and | 244 multi_addition = (isinstance(node.op, ast.Add) and |
| 239 isinstance(node.left, ast.BinOp) and | 245 isinstance(node.left, ast.BinOp) and |
| 240 isinstance(node.left.op, ast.Add)) | 246 isinstance(node.left.op, ast.Add)) |
| 241 if multi_addition and (isinstance(node.left.left, ast.Str) or | 247 if multi_addition and (isinstance(node.left.left, ast.Str) or |
| 242 isinstance(node.left.right, ast.Str) or | 248 isinstance(node.left.right, ast.Str) or |
| 243 isinstance(node.right, ast.Str)): | 249 isinstance(node.right, ast.Str)): |
| 244 self.errors.append((node, 'A108 use format() instead of ' | 250 self.errors.append((node, 'A108 use format() instead of ' |
| 245 '+ operator when concatenating ' | 251 '+ operator when concatenating ' |
| 246 '>2 strings')) | 252 'more than two strings')) |
|
Vasily Kuznetsov
2016/05/09 13:39:51
Maybe write it out: "more than two strings"? It's
Sebastian Noack
2016/05/09 16:49:11
I had it spelled out initially, but the message is
Vasily Kuznetsov
2016/05/09 17:37:32
I see. I don't feel too strongly about it, so up t
| |
| 247 | 253 |
| 248 self.generic_visit(node) | 254 self.generic_visit(node) |
| 249 | 255 |
| 250 def visit_Compare(self, node): | 256 def visit_Compare(self, node): |
| 251 left = node.left | 257 left = node.left |
| 252 single = len(node.ops) == 1 | 258 single = len(node.ops) == 1 |
| 253 | 259 |
| 254 for op, right in zip(node.ops, node.comparators): | 260 for op, right in zip(node.ops, node.comparators): |
| 255 membership = isinstance(op, (ast.In, ast.NotIn)) | 261 membership = isinstance(op, (ast.In, ast.NotIn)) |
| 256 symmetric = isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot)) | 262 symmetric = isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot)) |
| 257 | 263 |
| 258 if membership and isinstance(right, (ast.Tuple, ast.List)): | 264 if membership and isinstance(right, (ast.Tuple, ast.List)): |
| 259 self.errors.append((right, 'A102 use sets for distinct ' | 265 self.errors.append((right, 'A102 use sets for distinct ' |
| 260 'unordered data')) | 266 'unordered data')) |
| 261 | 267 |
| 262 consts_first = single and not membership or symmetric | 268 consts_first = single and not membership or symmetric |
| 263 if consts_first and is_const(left) and not is_const(right): | 269 if consts_first and is_const(left) and not is_const(right): |
| 264 self.errors.append((left, 'A103 yoda condition')) | 270 self.errors.append((left, 'A103 yoda condition')) |
| 265 | 271 |
| 266 left = right | 272 left = right |
| 267 | 273 |
| 268 self.generic_visit(node) | 274 self.generic_visit(node) |
| 269 | 275 |
| 270 def _check_deprecated(self, node, name): | 276 def _check_deprecated(self, node, name): |
| 271 substitute = DISCOURAGED_APIS.get(name) | 277 substitute = DISCOURAGED_APIS.get(name) |
| 272 if substitute: | 278 if substitute: |
| 273 self.errors.append((node, 'A301 use {}() instead ' | 279 self.errors.append((node, 'A301 use {}() instead of ' |
|
Vasily Kuznetsov
2016/05/09 13:39:51
instead -> instead of
Sebastian Noack
2016/05/09 16:49:12
Done.
| |
| 274 '{}()'.format(substitute, name))) | 280 '{}()'.format(substitute, name))) |
| 275 | 281 |
| 276 def visit_Call(self, node): | 282 def visit_Call(self, node): |
| 277 func = get_identifier(node.func) | 283 func = get_identifier(node.func) |
| 278 arg = next(iter(node.args), None) | 284 arg = next(iter(node.args), None) |
| 279 redundant_literal = False | 285 redundant_literal = False |
| 280 | 286 |
| 281 if isinstance(arg, ast.Lambda) and func in {'map', 'filter', | 287 if isinstance(arg, ast.Lambda) and func in {'map', 'filter', |
| 282 'imap', 'ifilter', | 288 'imap', 'ifilter', |
| 283 'itertools.imap', | 289 'itertools.imap', |
| 284 'itertools.ifilter'}: | 290 'itertools.ifilter'}: |
| 285 self.errors.append((node, 'A104 use a comprehension ' | 291 self.errors.append((node, 'A104 use a comprehension ' |
| 286 'instead calling {}() with ' | 292 'instead of calling {}() with ' |
|
Vasily Kuznetsov
2016/05/09 13:39:50
instead -> instead of
Sebastian Noack
2016/05/09 16:49:11
Done.
| |
| 287 'lambda function'.format(func))) | 293 'lambda function'.format(func))) |
| 288 elif isinstance(arg, (ast.List, ast.Tuple)): | 294 elif isinstance(arg, (ast.List, ast.Tuple)): |
| 289 if func == 'dict': | 295 if func == 'dict': |
| 290 redundant_literal = all(isinstance(elt, (ast.Tuple, ast.List)) | 296 redundant_literal = all(isinstance(elt, (ast.Tuple, ast.List)) |
| 291 for elt in arg.elts) | 297 for elt in arg.elts) |
| 292 else: | 298 else: |
| 293 redundant_literal = func in {'list', 'set', 'tuple'} | 299 redundant_literal = func in {'list', 'set', 'tuple'} |
| 294 elif isinstance(arg, (ast.ListComp, ast.GeneratorExp)): | 300 elif isinstance(arg, (ast.ListComp, ast.GeneratorExp)): |
| 295 if func == 'dict': | 301 if func == 'dict': |
| 296 redundant_literal = isinstance(arg.elt, (ast.Tuple, ast.List)) | 302 redundant_literal = isinstance(arg.elt, (ast.Tuple, ast.List)) |
| 297 else: | 303 else: |
| 298 redundant_literal = func in {'list', 'set'} | 304 redundant_literal = func in {'list', 'set'} |
| 299 | 305 |
| 300 if redundant_literal: | 306 if redundant_literal: |
| 301 self.errors.append((node, 'A105 use a {0} literal or ' | 307 self.errors.append((node, 'A105 use a {0} literal or ' |
| 302 'comprehension instead calling ' | 308 'comprehension instead of calling ' |
|
Vasily Kuznetsov
2016/05/09 13:39:51
instead -> instead of
Sebastian Noack
2016/05/09 16:49:11
Done.
| |
| 303 '{0}()'.format(func))) | 309 '{0}()'.format(func))) |
| 304 | 310 |
| 305 self._check_deprecated(node, func) | 311 self._check_deprecated(node, func) |
| 306 self.generic_visit(node) | 312 self.generic_visit(node) |
| 307 | 313 |
| 308 def visit_Import(self, node): | 314 def visit_Import(self, node): |
| 309 for alias in node.names: | 315 for alias in node.names: |
| 310 self._visit_stored_name(node, alias.asname or alias.name) | 316 self._visit_stored_name(node, alias.asname or alias.name) |
| 311 | 317 |
| 312 if hasattr(node, 'module'): | 318 if hasattr(node, 'module'): |
| (...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 391 yield (start, "A109 don't use u'', b'' " | 397 yield (start, "A109 don't use u'', b'' " |
| 392 "or r'' for doc strings") | 398 "or r'' for doc strings") |
| 393 elif start[0] == end[0]: | 399 elif start[0] == end[0]: |
| 394 if is_raw: | 400 if is_raw: |
| 395 literal = re.sub(r'\\(?!{})'.format(literal[0]), | 401 literal = re.sub(r'\\(?!{})'.format(literal[0]), |
| 396 '\\\\\\\\', literal) | 402 '\\\\\\\\', literal) |
| 397 | 403 |
| 398 if sys.version_info[0] >= 3: | 404 if sys.version_info[0] >= 3: |
| 399 if is_bytes: | 405 if is_bytes: |
| 400 literal = 'b' + literal | 406 literal = 'b' + literal |
| 401 else: | |
| 402 literal = re.sub(r'(?<!\\)\\x(?!a[0d])([a-f][0-9a-f])', | |
| 403 lambda m: chr(int(m.group(1), 16)), | |
| 404 literal) | |
| 405 elif is_unicode: | 407 elif is_unicode: |
| 406 literal = 'u' + literal | 408 literal = 'u' + literal |
| 407 | 409 |
| 408 if repr(eval(literal)) != literal: | 410 if ascii(eval(literal)) != literal: |
| 409 yield (start, "A110 string literal doesn't match repr()") | 411 yield (start, "A110 string literal doesn't match" |
|
Vasily Kuznetsov
2016/05/09 13:39:51
Maybe a bit clearer: "string literal doesn't match
Sebastian Noack
2016/05/09 16:49:12
I think that would be inaccurate. A string (litera
Vasily Kuznetsov
2016/05/09 17:37:32
So is it about the difference between a string and
Sebastian Noack
2016/05/11 10:50:09
Yeah, I disagree. But also note that on Python 3 t
Vasily Kuznetsov
2016/05/11 14:09:17
Yeah, I agree with you that the proposed Python 3
| |
| 412 '{}()'.format(ascii.__name__)) | |
| 410 | 413 |
| 411 first_token = False | 414 first_token = False |
| 412 | 415 |
| 413 check_quotes.name = 'abp-quotes' | 416 check_quotes.name = 'abp-quotes' |
| 414 check_quotes.version = __version__ | 417 check_quotes.version = __version__ |
| 415 | 418 |
| 416 | 419 |
| 417 def check_redundant_parenthesis(logical_line, tokens): | 420 def check_redundant_parenthesis(logical_line, tokens): |
| 418 start_line = tokens[0][2][0] | 421 start_line = tokens[0][2][0] |
| 419 level = 0 | 422 level = 0 |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 459 if tokens[i + 1][:2] != (tokenize.OP, ':'): | 462 if tokens[i + 1][:2] != (tokenize.OP, ':'): |
| 460 break | 463 break |
| 461 | 464 |
| 462 return [(pos, 'A111 redundant parenthesis for {} ' | 465 return [(pos, 'A111 redundant parenthesis for {} ' |
| 463 'statement'.format(statement))] | 466 'statement'.format(statement))] |
| 464 | 467 |
| 465 return [] | 468 return [] |
| 466 | 469 |
| 467 check_redundant_parenthesis.name = 'abp-redundant-parenthesis' | 470 check_redundant_parenthesis.name = 'abp-redundant-parenthesis' |
| 468 check_redundant_parenthesis.version = __version__ | 471 check_redundant_parenthesis.version = __version__ |
| LEFT | RIGHT |