Skip to content

Commit fb588c9

Browse files
committed
Add check for consistent returns PyCQA#399
First attempt to implement new check about consistent returns. PEP 8 has been updated accordingly (cf GvR message). Implementation is still perfectible on many points. Also, I've changed many details here and there that are not relevant for the issue. Finally, I'll need to double-check the error number.
1 parent 151758c commit fb588c9

File tree

13 files changed

+233
-55
lines changed

13 files changed

+233
-55
lines changed

Diff for: docs/intro.rst

+7
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,13 @@ This is the current list of error and warning codes:
407407
+----------+----------------------------------------------------------------------+
408408
| W604 | backticks are deprecated, use 'repr()' |
409409
+----------+----------------------------------------------------------------------+
410+
+----------+----------------------------------------------------------------------+
411+
| **W7** | *Statement warning* |
412+
+----------+----------------------------------------------------------------------+
413+
| W700 | inconsistent use of return (explicit) |
414+
+----------+----------------------------------------------------------------------+
415+
| W701 | inconsistent use of return (implicit on reachable end of function) |
416+
+----------+----------------------------------------------------------------------+
410417

411418

412419
**(*)** In the default configuration, the checks **E121**, **E123**, **E126**,

Diff for: pep8.py

+157-53
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import inspect
5555
import keyword
5656
import tokenize
57+
import ast
5758
from optparse import OptionParser
5859
from fnmatch import fnmatch
5960
try:
@@ -752,16 +753,13 @@ def whitespace_around_named_parameter_equals(logical_line, tokens):
752753
Don't use spaces around the '=' sign when used to indicate a
753754
keyword argument or a default parameter value.
754755
755-
Okay: def complex(real, imag=0.0):
756-
Okay: return magic(r=real, i=imag)
756+
Okay: def complex(real, imag=0.0):\n return magic(r=real, i=imag)
757757
Okay: boolean(a == b)
758758
Okay: boolean(a != b)
759759
Okay: boolean(a <= b)
760760
Okay: boolean(a >= b)
761-
Okay: def foo(arg: int = 42):
762-
763-
E251: def complex(real, imag = 0.0):
764-
E251: return magic(r = real, i = imag)
761+
Okay: def foo(arg: int = 42):\n pass
762+
E251: def complex(real, imag = 0.0):\n return magic(r = real, i = imag)
765763
"""
766764
parens = 0
767765
no_space = False
@@ -781,9 +779,9 @@ def whitespace_around_named_parameter_equals(logical_line, tokens):
781779
parens += 1
782780
elif text == ')':
783781
parens -= 1
784-
elif in_def and text == ':' and parens == 1:
782+
elif parens == 1 and in_def and text == ':':
785783
annotated_func_arg = True
786-
elif parens and text == ',' and parens == 1:
784+
elif parens == 1 and text == ',':
787785
annotated_func_arg = False
788786
elif parens and text == '=' and not annotated_func_arg:
789787
no_space = True
@@ -1013,7 +1011,7 @@ def break_around_binary_operator(logical_line, tokens):
10131011
10141012
Okay: (width == 0 +\n height == 0)
10151013
Okay: foo(\n -x)
1016-
Okay: foo(x\n [])
1014+
Okay: foo(x,\n [])
10171015
Okay: x = '''\n''' + ''
10181016
Okay: foo(x,\n -y)
10191017
Okay: foo(x, # comment\n -y)
@@ -1046,11 +1044,11 @@ def comparison_to_singleton(logical_line, noqa):
10461044
Comparisons to singletons like None should always be done
10471045
with "is" or "is not", never the equality operators.
10481046
1049-
Okay: if arg is not None:
1050-
E711: if arg != None:
1051-
E711: if None == arg:
1052-
E712: if arg == True:
1053-
E712: if False == arg:
1047+
Okay: arg is not None
1048+
E711: arg != None
1049+
E711: None == arg
1050+
E712: arg == True
1051+
E712: False == arg
10541052
10551053
Also, beware of writing if x when you really mean if x is not None --
10561054
e.g. when testing whether a variable or argument that defaults to None was
@@ -1100,15 +1098,15 @@ def comparison_type(logical_line, noqa):
11001098
11011099
Do not compare types directly.
11021100
1103-
Okay: if isinstance(obj, int):
1104-
E721: if type(obj) is type(1):
1101+
Okay: isinstance(obj, int)
1102+
E721: type(obj) is type(1)
11051103
11061104
When checking if an object is a string, keep in mind that it might be a
11071105
unicode string too! In Python 2.3, str and unicode have a common base
11081106
class, basestring, so you can do:
11091107
1110-
Okay: if isinstance(obj, basestring):
1111-
Okay: if type(a1) is type(b1):
1108+
Okay: isinstance(obj, basestring)
1109+
Okay: type(a1) is type(b1)
11121110
"""
11131111
match = COMPARE_TYPE_REGEX.search(logical_line)
11141112
if match and not noqa:
@@ -1121,7 +1119,7 @@ def comparison_type(logical_line, noqa):
11211119
def python_3000_has_key(logical_line, noqa):
11221120
r"""The {}.has_key() method is removed in Python 3: use the 'in' operator.
11231121
1124-
Okay: if "alph" in d:\n print d["alph"]
1122+
Okay: "alph" in d
11251123
W601: assert d.has_key('alph')
11261124
"""
11271125
pos = logical_line.find('.has_key(')
@@ -1147,8 +1145,8 @@ def python_3000_not_equal(logical_line):
11471145
11481146
The older syntax is removed in Python 3.
11491147
1150-
Okay: if a != 'no':
1151-
W603: if a <> 'no':
1148+
Okay: a != 'no'
1149+
W603: a <> 'no'
11521150
"""
11531151
pos = logical_line.find('<>')
11541152
if pos > -1:
@@ -1166,6 +1164,105 @@ def python_3000_backticks(logical_line):
11661164
yield pos, "W604 backticks are deprecated, use 'repr()'"
11671165

11681166

1167+
def inexisting_last_line_is_reachable(tree):
1168+
r"""Return true if inexisting last line of ast tree is reachable.
1169+
1170+
Detecting whether the last line of some piece of code is reachable or not
1171+
corresponds to solving the halting problem which is known to be impossible.
1172+
However, we could solve a relaxed version of this : indeed, we may assume
1173+
that:
1174+
- all code is written for a reason and is supposed to be reachable.
1175+
- only a limited amount of statements may break the reachable property:
1176+
* return statements
1177+
* raise statements
1178+
* assert False.
1179+
We'll consider the last line to be reachable if TODO
1180+
"""
1181+
assert isinstance(tree, ast.AST)
1182+
children = list(ast.iter_child_nodes(tree))
1183+
last_child = children[-1]
1184+
# These stop reachability
1185+
if isinstance(last_child, ast.Return):
1186+
return False
1187+
elif isinstance(last_child, ast.Raise):
1188+
return False
1189+
elif isinstance(last_child, ast.Assert):
1190+
test_val = last_child.test
1191+
if isinstance(test_val, ast.Name) and test_val.id == "False":
1192+
return False
1193+
if isinstance(test_val, ast.NameConstant) and not test_val.value:
1194+
return False
1195+
# These propagage reachability
1196+
elif isinstance(last_child, ast.If):
1197+
then_body, else_body = last_child.body, last_child.orelse
1198+
if not else_body:
1199+
return True
1200+
return any(inexisting_last_line_is_reachable(branch[-1])
1201+
for branch in (then_body, else_body))
1202+
# elif isinstance(last_child, ast.For):
1203+
# pass # print(last_child)
1204+
# elif isinstance(last_child, ast.While):
1205+
# pass # print(last_child)
1206+
# Otherwise, it is reachable
1207+
print(last_child)
1208+
return False # will be true at the end
1209+
1210+
1211+
class UnconsistentReturns():
1212+
r"""This doc mentions W700 and W701."""
1213+
1214+
def __init__(self, tree, filename):
1215+
r"""This doc mentions W700 and W701."""
1216+
# print("init1")
1217+
self.tree = tree
1218+
self.filename = filename
1219+
1220+
def run(self):
1221+
r"""Run the check."""
1222+
return UnconsistentReturns.check_in_tree(self.tree)
1223+
1224+
@staticmethod
1225+
def check_in_tree(tree):
1226+
r"""Check for inconsistent returns in tree."""
1227+
assert isinstance(tree, ast.AST)
1228+
for node in ast.walk(tree):
1229+
if isinstance(node, ast.FunctionDef):
1230+
for err in UnconsistentReturns.check_in_func(node):
1231+
yield err
1232+
1233+
@staticmethod
1234+
def check_in_func(func_node):
1235+
r"""Check for inconsistent returns (with or without values) in function.
1236+
1237+
Functions should either return an explicit value in all return
1238+
statements (including the final value-less implicit return if
1239+
reachable) or in none of them.
1240+
"""
1241+
assert isinstance(func_node, ast.FunctionDef)
1242+
returns = UnconsistentReturns.collect_return_nodes(func_node)
1243+
returns_value = [ret for ret in returns if ret.value is not None]
1244+
if returns_value:
1245+
for r in returns:
1246+
if r.value is None:
1247+
yield (r.lineno, r.col_offset,
1248+
"W700 unconsistent return values", "toto")
1249+
if inexisting_last_line_is_reachable(func_node):
1250+
yield (func_node.lineno, func_node.col_offset,
1251+
"W701 unconsistent return values", "toto")
1252+
1253+
@staticmethod
1254+
def collect_return_nodes(func_node):
1255+
r"""Collect return nodes from the node describing a function.
1256+
1257+
The tricky part is not to get the nodes corresponding to return
1258+
statements in nested function definitions.
1259+
"""
1260+
# THIS DOES NOT HANDLE NESTED FUNCTIONS PROPERLY
1261+
assert isinstance(func_node, ast.FunctionDef)
1262+
return [node for node in ast.walk(func_node)
1263+
if isinstance(node, ast.Return)]
1264+
1265+
11691266
##############################################################################
11701267
# Helper functions
11711268
##############################################################################
@@ -1321,32 +1418,40 @@ def _get_parameters(function):
13211418
return inspect.getargspec(function)[0]
13221419

13231420

1324-
def register_check(check, codes=None):
1421+
def register_check(check):
13251422
"""Register a new check object."""
1326-
def _add_check(check, kind, codes, args):
1423+
def _add_check(check, kind, args):
1424+
# print(check, kind, len(_checks[kind]))
1425+
codes = ERRORCODE_REGEX.findall(check.__doc__ or '')
13271426
if check in _checks[kind]:
13281427
_checks[kind][check][0].extend(codes or [])
13291428
else:
13301429
_checks[kind][check] = (codes or [''], args)
1430+
# print("_add_check", kind, check.__name__, len(_checks[kind]))
13311431
if inspect.isfunction(check):
13321432
args = _get_parameters(check)
13331433
if args and args[0] in ('physical_line', 'logical_line'):
1334-
if codes is None:
1335-
codes = ERRORCODE_REGEX.findall(check.__doc__ or '')
1336-
_add_check(check, args[0], codes, args)
1434+
_add_check(check, args[0], args)
13371435
elif inspect.isclass(check):
1338-
if _get_parameters(check.__init__)[:2] == ['self', 'tree']:
1339-
_add_check(check, 'tree', codes, None)
1436+
if check.__module__ in ("pep8", "__main__"): # HACK
1437+
args = _get_parameters(check.__init__)
1438+
if args and args[0] == 'self' and args[1] == 'tree':
1439+
# print(check, args)
1440+
_add_check(check, args[1], args)
13401441

13411442

13421443
def init_checks_registry():
13431444
"""Register all globally visible functions.
13441445
13451446
The first argument name is either 'physical_line' or 'logical_line'.
13461447
"""
1448+
print("init_checks_registry begin")
13471449
mod = inspect.getmodule(register_check)
13481450
for (name, function) in inspect.getmembers(mod, inspect.isfunction):
13491451
register_check(function)
1452+
for (name, klass) in inspect.getmembers(mod, inspect.isclass):
1453+
register_check(klass)
1454+
print("init_checks_registry end")
13501455
init_checks_registry()
13511456

13521457

@@ -1363,6 +1468,7 @@ def __init__(self, filename=None, lines=None,
13631468
self._physical_checks = options.physical_checks
13641469
self._logical_checks = options.logical_checks
13651470
self._ast_checks = options.ast_checks
1471+
# print("options.ast_checks", options.ast_checks)
13661472
self.max_line_length = options.max_line_length
13671473
self.multiline = False # in a multiline string?
13681474
self.hang_closing = options.hang_closing
@@ -1420,21 +1526,16 @@ def readline(self):
14201526

14211527
def run_check(self, check, argument_names):
14221528
"""Run a check plugin."""
1423-
arguments = []
1424-
for name in argument_names:
1425-
arguments.append(getattr(self, name))
1426-
return check(*arguments)
1427-
1428-
def init_checker_state(self, name, argument_names):
1429-
""" Prepares a custom state for the specific checker plugin."""
14301529
if 'checker_state' in argument_names:
1431-
self.checker_state = self._checker_states.setdefault(name, {})
1530+
self.checker_state = self._checker_states.setdefault(
1531+
check.__name__, {})
1532+
arguments = [getattr(self, name) for name in argument_names]
1533+
return check(*arguments)
14321534

14331535
def check_physical(self, line):
14341536
"""Run all physical checks on a raw input line."""
14351537
self.physical_line = line
1436-
for name, check, argument_names in self._physical_checks:
1437-
self.init_checker_state(name, argument_names)
1538+
for check, argument_names in self._physical_checks:
14381539
result = self.run_check(check, argument_names)
14391540
if result is not None:
14401541
(offset, text) = result
@@ -1490,10 +1591,7 @@ def check_logical(self):
14901591
self.blank_before = self.blank_lines
14911592
if self.verbose >= 2:
14921593
print(self.logical_line[:80].rstrip())
1493-
for name, check, argument_names in self._logical_checks:
1494-
if self.verbose >= 4:
1495-
print(' ' + name)
1496-
self.init_checker_state(name, argument_names)
1594+
for check, argument_names in self._logical_checks:
14971595
for offset, text in self.run_check(check, argument_names) or ():
14981596
if not isinstance(offset, tuple):
14991597
for token_offset, pos in mapping:
@@ -1509,11 +1607,13 @@ def check_logical(self):
15091607

15101608
def check_ast(self):
15111609
"""Build the file's AST and run all AST checks."""
1610+
# print("check_ast")
15121611
try:
15131612
tree = compile(''.join(self.lines), '', 'exec', PyCF_ONLY_AST)
15141613
except (ValueError, SyntaxError, TypeError):
15151614
return self.report_invalid_syntax()
1516-
for name, cls, __ in self._ast_checks:
1615+
for cls, __ in self._ast_checks:
1616+
# print("toto", cls)
15171617
checker = cls(tree, self.filename)
15181618
for lineno, offset, text, check in checker.run():
15191619
if not self.lines or not noqa(self.lines[lineno - 1]):
@@ -1565,9 +1665,11 @@ def maybe_check_physical(self, token):
15651665

15661666
def check_all(self, expected=None, line_offset=0):
15671667
"""Run all checks on the input file."""
1668+
# print("check_all")
15681669
self.report.init_file(self.filename, self.lines, expected, line_offset)
15691670
self.total_lines = len(self.lines)
15701671
if self._ast_checks:
1672+
# print("toto3")
15711673
self.check_ast()
15721674
self.line_number = 0
15731675
self.indent_char = None
@@ -1817,6 +1919,9 @@ def __init__(self, *args, **kwargs):
18171919
options.physical_checks = self.get_checks('physical_line')
18181920
options.logical_checks = self.get_checks('logical_line')
18191921
options.ast_checks = self.get_checks('tree')
1922+
# print("options.physical_checks", len(options.physical_checks))
1923+
# print("options.logical_checks", len(options.logical_checks))
1924+
# print("options.ast_checks", len(options.ast_checks))
18201925
self.init_report()
18211926

18221927
def init_report(self, reporter=None):
@@ -1894,24 +1999,23 @@ def ignore_code(self, code):
18941999
return False. Else, if 'options.ignore' contains a prefix of
18952000
the error code, return True.
18962001
"""
1897-
if len(code) < 4 and any(s.startswith(code)
1898-
for s in self.options.select):
2002+
options = self.options
2003+
if len(code) < 4 and any(s.startswith(code) for s in options.select):
18992004
return False
1900-
return (code.startswith(self.options.ignore) and
1901-
not code.startswith(self.options.select))
2005+
return (code.startswith(options.ignore) and
2006+
not code.startswith(options.select))
19022007

19032008
def get_checks(self, argument_name):
19042009
"""Get all the checks for this category.
19052010
19062011
Find all globally visible functions where the first argument name
19072012
starts with argument_name and which contain selected tests.
19082013
"""
1909-
checks = []
1910-
for check, attrs in _checks[argument_name].items():
1911-
(codes, args) = attrs
1912-
if any(not (code and self.ignore_code(code)) for code in codes):
1913-
checks.append((check.__name__, check, args))
1914-
return sorted(checks)
2014+
checks = [(check, args)
2015+
for check, (codes, args) in _checks[argument_name].items()
2016+
if any(not self.ignore_code(code) for code in codes)]
2017+
print(argument_name, len(checks), len(_checks[argument_name]))
2018+
return sorted(checks, key=lambda arg: arg[0].__name__)
19152019

19162020

19172021
def get_parser(prog='pep8', version=__version__):

0 commit comments

Comments
 (0)