Skip to content

Commit 56a6723

Browse files
hippo91PCManticore
authored andcommitted
Adding a check for inconsistent-return-statements inside function or methods. (#1641)
Close #1267
1 parent 8dd70cb commit 56a6723

29 files changed

+342
-43
lines changed

ChangeLog

+3
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ What's New in Pylint 1.8?
102102

103103
Close #1336
104104

105+
* Added a new Python check for inconsistent return statements inside method or function.
106+
Close #1267
107+
105108
* Fix ``superfluous-parens`` false positive related to handling logical statements
106109
involving ``in`` operator.
107110

doc/whatsnew/1.8.rst

+28
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,34 @@ New checkers
219219
var = "123"
220220
logging.log(logging.INFO, "Var: %s", var)
221221
222+
* A new Python checker was added to warn about ``inconsistent-return-statements``. A function or a method
223+
has inconsistent return statements if it returns both explicit and implicit values :
224+
225+
.. code-block:: python
226+
227+
def mix_implicit_explicit_returns(arg):
228+
if arg < 10:
229+
return True
230+
elif arg < 20:
231+
return
232+
233+
According to PEP8_, if any return statement returns an expression,
234+
any return statements where no value is returned should explicitly state this as return None,
235+
and an explicit return statement should be present at the end of the function (if reachable).
236+
Thus, the previous function should be written:
237+
238+
.. code-block:: python
239+
240+
def mix_implicit_explicit_returns(arg):
241+
if arg < 10:
242+
return True
243+
elif arg < 20:
244+
return None
245+
246+
Close #1267
247+
248+
.. _PEP8: https://www.python.org/dev/peps/pep-0008
249+
222250
Other Changes
223251
=============
224252

pylint/checkers/classes.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,14 @@ def _safe_infer_call_result(node, caller, context=None):
298298
inferit = node.infer_call_result(caller, context=context)
299299
value = next(inferit)
300300
except astroid.InferenceError:
301-
return # inference failed
301+
return None # inference failed
302302
except StopIteration:
303-
return # no values infered
303+
return None # no values infered
304304
try:
305305
next(inferit)
306-
return # there is ambiguity on the inferred node
306+
return None # there is ambiguity on the inferred node
307307
except astroid.InferenceError:
308-
return # there is some kind of ambiguity
308+
return None # there is some kind of ambiguity
309309
except StopIteration:
310310
return value
311311

pylint/checkers/format.py

+1
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,7 @@ def check_indent_level(self, string, expected, line_num):
10641064
self.add_message('bad-indentation', line=line_num,
10651065
args=(level * unit_size + len(suppl), i_type,
10661066
expected * unit_size))
1067+
return None
10671068

10681069

10691070
def register(linter):

pylint/checkers/imports.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def _get_first_import(node, context, name, base, level, alias):
9797
break
9898
if found and not are_exclusive(first, node):
9999
return first
100+
return None
100101

101102

102103
def _ignore_import_failure(node, modname, ignored_modules):
@@ -629,7 +630,7 @@ def _check_relative_import(self, modnode, importnode, importedmodnode,
629630
the imported module name.
630631
"""
631632
if not self.linter.is_message_enabled('relative-import'):
632-
return
633+
return None
633634
if importedmodnode.file is None:
634635
return False # built-in module
635636
if modnode is importedmodnode:
@@ -641,6 +642,8 @@ def _check_relative_import(self, modnode, importnode, importedmodnode,
641642
self.add_message('relative-import',
642643
args=(importedasname, importedmodnode.name),
643644
node=importnode)
645+
return None
646+
return None
644647

645648
def _add_imported_module(self, node, importedmodname):
646649
"""notify an imported module, used to analyze dependencies"""

pylint/checkers/python3.py

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def _is_old_octal(literal):
3636
except ValueError:
3737
return False
3838
return True
39+
return None
3940

4041

4142
def _check_dict_node(node):
@@ -892,6 +893,7 @@ def _check_raise_value(self, node, expr):
892893
if isinstance(value, str):
893894
self.add_message('raising-string', node=node)
894895
return True
896+
return None
895897

896898

897899
class Python3TokenChecker(checkers.BaseTokenChecker):

pylint/checkers/refactoring.py

+80-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,45 @@ def _all_elements_are_true(gen):
2727
return values and all(values)
2828

2929

30+
def _is_node_return_ended(node):
31+
"""Check if the node ends with an explicit return statement.
32+
33+
Args:
34+
node (astroid.NodeNG): node to be checked.
35+
36+
Returns:
37+
bool: True if the node ends with an explicit statement, False otherwise.
38+
39+
"""
40+
# Recursion base case
41+
if isinstance(node, astroid.Return):
42+
return True
43+
if isinstance(node, astroid.Raise):
44+
# a Raise statement doesn't need to end with a return statement
45+
# but if the exception raised is handled, then the handler has to
46+
# ends with a return statement
47+
exc = utils.safe_infer(node.exc)
48+
if exc is None or exc is astroid.Uninferable:
49+
return False
50+
exc_name = exc.pytype().split('.')[-1]
51+
handlers = utils.get_exception_handlers(node, exc_name)
52+
if handlers:
53+
# among all the handlers handling the exception at least one
54+
# must end with a return statement
55+
return any(_is_node_return_ended(_handler) for _handler in handlers)
56+
# if no handlers handle the exception then it's ok
57+
return True
58+
if isinstance(node, astroid.If):
59+
# if statement is returning if there are exactly two return statements in its
60+
# children : one for the body part, the other for the orelse part
61+
return_stmts = [_is_node_return_ended(_child) for _child in node.get_children()]
62+
return sum(return_stmts) == 2
63+
# recurses on the children of the node except for those which are except handler
64+
# because one cannot be sure that the handler will really be used
65+
return any(_is_node_return_ended(_child) for _child in node.get_children()
66+
if not isinstance(_child, astroid.ExceptHandler))
67+
68+
3069
def _if_statement_is_always_returning(if_node):
3170
def _has_return_node(elems, scope):
3271
for node in elems:
@@ -111,6 +150,14 @@ class RefactoringChecker(checkers.BaseTokenChecker):
111150
'a generator may lead to hard to find bugs. This PEP specify that '
112151
'raise StopIteration has to be replaced by a simple return statement',
113152
{'minversion': (3, 0)}),
153+
'R1710': ('Either all return statements in a function should return an expression, '
154+
'or none of them should.',
155+
'inconsistent-return-statements',
156+
'According to PEP8, if any return statement returns an expression, '
157+
'any return statements where no value is returned should explicitly '
158+
'state this as return None, and an explicit return statement '
159+
'should be present at the end of the function (if reachable)'
160+
),
114161
}
115162
options = (('max-nested-blocks',
116163
{'default': 5, 'type': 'int', 'metavar': '<int>',
@@ -122,6 +169,7 @@ class RefactoringChecker(checkers.BaseTokenChecker):
122169

123170
def __init__(self, linter=None):
124171
checkers.BaseTokenChecker.__init__(self, linter)
172+
self._return_nodes = {}
125173
self._init()
126174

127175
def _init(self):
@@ -309,12 +357,15 @@ def visit_if(self, node):
309357
self._check_nested_blocks(node)
310358
self._check_superfluous_else_return(node)
311359

312-
@utils.check_messages('too-many-nested-blocks')
313-
def leave_functiondef(self, _):
360+
@utils.check_messages('too-many-nested-blocks', 'inconsistent-return-statements')
361+
def leave_functiondef(self, node):
314362
# check left-over nested blocks stack
315363
self._emit_nested_blocks_message_if_needed(self._nested_blocks)
316364
# new scope = reinitialize the stack of nested blocks
317365
self._nested_blocks = []
366+
# check consistent return statements
367+
self._check_consistent_returns(node)
368+
self._return_nodes[node.name] = []
318369

319370
def visit_raise(self, node):
320371
self._check_stop_iteration_inside_generator(node)
@@ -491,6 +542,33 @@ def _seq_based_ternary_params(node):
491542
condition = node.slice.value
492543
return condition, true_value, false_value
493544

545+
def visit_functiondef(self, node):
546+
self._return_nodes[node.name] = []
547+
return_nodes = node.nodes_of_class(astroid.Return)
548+
self._return_nodes[node.name] = [_rnode for _rnode in return_nodes
549+
if _rnode.frame() == node.frame()]
550+
551+
def _check_consistent_returns(self, node):
552+
"""Check that all return statements inside a function are consistent.
553+
554+
Return statements are consistent if:
555+
- all returns are explicit and if there is no implicit return;
556+
- all returns are empty and if there is, possibly, an implicit return.
557+
558+
Args:
559+
node (astroid.FunctionDef): the function holding the return statements.
560+
561+
"""
562+
# explicit return statements are those with a not None value
563+
explicit_returns = [_node for _node in self._return_nodes[node.name]
564+
if _node.value is not None]
565+
if not explicit_returns:
566+
return
567+
if (len(explicit_returns) == len(self._return_nodes[node.name])
568+
and _is_node_return_ended(node)):
569+
return
570+
self.add_message('inconsistent-return-statements', node=node)
571+
494572

495573
class RecommandationChecker(checkers.BaseChecker):
496574
__implements__ = (interfaces.IAstroidChecker,)

pylint/checkers/typecheck.py

+14-13
Original file line numberDiff line numberDiff line change
@@ -1002,15 +1002,15 @@ def visit_extslice(self, node):
10021002
@check_messages('invalid-sequence-index')
10031003
def visit_index(self, node):
10041004
if not node.parent or not hasattr(node.parent, "value"):
1005-
return
1005+
return None
10061006
# Look for index operations where the parent is a sequence type.
10071007
# If the types can be determined, only allow indices to be int,
10081008
# slice or instances with __index__.
10091009
parent_type = safe_infer(node.parent.value)
10101010
if not isinstance(parent_type, (astroid.ClassDef, astroid.Instance)):
1011-
return
1011+
return None
10121012
if not has_known_bases(parent_type):
1013-
return
1013+
return None
10141014

10151015
# Determine what method on the parent this index will use
10161016
# The parent of this node will be a Subscript, and the parent of that
@@ -1029,20 +1029,20 @@ def visit_index(self, node):
10291029
try:
10301030
methods = dunder_lookup.lookup(parent_type, methodname)
10311031
if methods is astroid.YES:
1032-
return
1032+
return None
10331033
itemmethod = methods[0]
10341034
except (exceptions.NotFoundError,
10351035
exceptions.AttributeInferenceError,
10361036
IndexError):
1037-
return
1037+
return None
10381038
if not isinstance(itemmethod, astroid.FunctionDef):
1039-
return
1039+
return None
10401040
if itemmethod.root().name != BUILTINS:
1041-
return
1041+
return None
10421042
if not itemmethod.parent:
1043-
return
1043+
return None
10441044
if itemmethod.parent.name not in SEQUENCE_TYPES:
1045-
return
1045+
return None
10461046

10471047
# For ExtSlice objects coming from visit_extslice, no further
10481048
# inference is necessary, since if we got this far the ExtSlice
@@ -1052,18 +1052,18 @@ def visit_index(self, node):
10521052
else:
10531053
index_type = safe_infer(node)
10541054
if index_type is None or index_type is astroid.YES:
1055-
return
1055+
return None
10561056
# Constants must be of type int
10571057
if isinstance(index_type, astroid.Const):
10581058
if isinstance(index_type.value, int):
1059-
return
1059+
return None
10601060
# Instance values must be int, slice, or have an __index__ method
10611061
elif isinstance(index_type, astroid.Instance):
10621062
if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.slice'):
1063-
return
1063+
return None
10641064
try:
10651065
index_type.getattr('__index__')
1066-
return
1066+
return None
10671067
except exceptions.NotFoundError:
10681068
pass
10691069
elif isinstance(index_type, astroid.Slice):
@@ -1074,6 +1074,7 @@ def visit_index(self, node):
10741074

10751075
# Anything else is an error
10761076
self.add_message('invalid-sequence-index', node=node)
1077+
return None
10771078

10781079
@check_messages('invalid-slice-index')
10791080
def visit_slice(self, node):

pylint/checkers/utils.py

+29-11
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ def _is_property_decorator(decorator):
505505
for ancestor in infered.ancestors():
506506
if ancestor.name == 'property' and ancestor.root().name == BUILTINS_NAME:
507507
return True
508+
return None
508509

509510

510511
def decorated_with(func, qnames):
@@ -610,21 +611,38 @@ def is_from_fallback_block(node):
610611

611612

612613
def _except_handlers_ignores_exception(handlers, exception):
613-
func = functools.partial(error_of_type,
614-
error_type=(exception, ))
614+
func = functools.partial(error_of_type, error_type=(exception, ))
615615
return any(map(func, handlers))
616616

617617

618-
def node_ignores_exception(node, exception):
619-
"""Check if the node is in a TryExcept which handles the given exception."""
618+
def get_exception_handlers(node, exception):
619+
"""Return the collections of handlers handling the exception in arguments.
620+
621+
Args:
622+
node (astroid.Raise): the node raising the exception.
623+
exception (builtin.Exception or str): exception or name of the exception.
624+
625+
Returns:
626+
generator: the collection of handlers that are handling the exception or None.
627+
628+
"""
620629
current = node
621630
ignores = (astroid.ExceptHandler, astroid.TryExcept)
622631
while current and not isinstance(current.parent, ignores):
623632
current = current.parent
624633

625634
if current and isinstance(current.parent, astroid.TryExcept):
626-
return _except_handlers_ignores_exception(current.parent.handlers, exception)
627-
return False
635+
return (_handler for _handler in current.parent.handlers
636+
if error_of_type(_handler, exception))
637+
return None
638+
639+
640+
def node_ignores_exception(node, exception):
641+
"""Check if the node is in a TryExcept which handles the given exception."""
642+
managing_handlers = get_exception_handlers(node, exception)
643+
if not managing_handlers:
644+
return False
645+
return any(managing_handlers)
628646

629647

630648
def class_is_abstract(node):
@@ -774,12 +792,12 @@ def safe_infer(node, context=None):
774792
inferit = node.infer(context=context)
775793
value = next(inferit)
776794
except astroid.InferenceError:
777-
return
795+
return None
778796
try:
779797
next(inferit)
780-
return # None if there is ambiguity on the inferred node
798+
return None # None if there is ambiguity on the inferred node
781799
except astroid.InferenceError:
782-
return # there is some kind of ambiguity
800+
return None # there is some kind of ambiguity
783801
except StopIteration:
784802
return value
785803

@@ -824,9 +842,9 @@ def node_type(node):
824842
continue
825843
types.add(var_type)
826844
if len(types) > 1:
827-
return
845+
return None
828846
except astroid.InferenceError:
829-
return
847+
return None
830848
return types.pop() if types else None
831849

832850

0 commit comments

Comments
 (0)