diff --git a/docs/intro.rst b/docs/intro.rst index 4ddf91f4..7821591c 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -373,6 +373,8 @@ This is the current list of error and warning codes: +------------+----------------------------------------------------------------------+ | E743 | do not define functions named 'l', 'O', or 'I' | +------------+----------------------------------------------------------------------+ +| E750 | do not mix 'return' and 'return value' in the same function | ++------------+----------------------------------------------------------------------+ +------------+----------------------------------------------------------------------+ | **E9** | *Runtime* | +------------+----------------------------------------------------------------------+ diff --git a/pycodestyle.py b/pycodestyle.py index 1b066911..ad14c8f1 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -281,7 +281,7 @@ def maximum_line_length(physical_line, max_line_length, multiline, noqa): if ((len(chunks) == 1 and multiline) or (len(chunks) == 2 and chunks[0] == '#')) and \ len(line) - len(chunks[-1]) < max_line_length - 7: - return + return None if hasattr(line, 'decode'): # Python 2 # The line could contain multi-byte characters try: @@ -495,6 +495,58 @@ def indentation(logical_line, previous_logical, indent_char, yield 0, tmpl % (3 + c, "unexpected indentation") +@register_check +def returns(logical_line, indent_level, previous_logical, checker_state): + r"""Be consistent in return statements. + + Either all return statements in a function should return an expression, or + none of them should. If any return statement returns an expression, any + return statements where no value is returned should explicitly state this + as return None, [and an explicit return statement should be present at the + end of the function (if reachable).] + + The reachability constraint is not implemented due to its complexity. + + Okay: def a():\n return 1 + Okay: def a():\n return 1\n return 2 + Okay: def a():\n return + Okay: def a():\n return\n return + Okay: def a():\n def b():\n return\n return b + Okay: def a():\n def b():\n return 2\n return + + E750: def a():\n return\n return 2 + E750: def a():\n return 4\n return + """ + functions_stack = checker_state.setdefault('functions_stack', []) + # a stack of functions, containing: + # indent_level, return_without_value, return_with_value + INDENT, RETURN_NO_VALUE, RETURN_VALUE = 0, 1, 2 + if STARTSWITH_DEF_REGEX.match(previous_logical): + functions_stack.append([indent_level, False, False]) + + if functions_stack and indent_level < functions_stack[-1][INDENT]: + functions_stack.pop() + + if logical_line.startswith('return'): + try: + last_fun_record = functions_stack[-1] + except IndexError: + # ignore return statements outside of functions (this happens in + # pycodestyle unit tests only) + return + + if logical_line == 'return': + if last_fun_record[RETURN_VALUE]: + yield 0, "E750 'return' without expression used in the same " \ + "method as 'return' with expression" + last_fun_record[RETURN_NO_VALUE] = True + else: + if last_fun_record[RETURN_NO_VALUE]: + yield 0, "E750 'return' with expression used in the same " \ + "method as 'return' without expression" + last_fun_record[RETURN_VALUE] = True + + @register_check def continued_indentation(logical_line, tokens, indent_level, hang_closing, indent_char, noqa, verbose): @@ -1910,7 +1962,7 @@ def error(self, line_number, offset, text, check): """Report an error, according to options.""" code = text[:4] if self._ignore_code(code): - return + return None if code in self.counters: self.counters[code] += 1 else: @@ -1918,7 +1970,7 @@ def error(self, line_number, offset, text, check): self.messages[code] = text[5:] # Don't care about expected errors or warnings if code in self.expected: - return + return None if self.print_filename and not self.file_errors: print(self.filename) self.file_errors += 1 @@ -2030,7 +2082,7 @@ def __init__(self, options): def error(self, line_number, offset, text, check): if line_number not in self._selected[self.filename]: - return + return None return super(DiffReport, self).error(line_number, offset, text, check) diff --git a/testsuite/E75.py b/testsuite/E75.py new file mode 100644 index 00000000..3ffa635a --- /dev/null +++ b/testsuite/E75.py @@ -0,0 +1,50 @@ +#: Okay +def okay_a(x): + if x: + return 1 + else: + return 2 +#: Okay +def okay_b(x): + if x: + x += 1 + return + z.append(x) + return +#: E750:5:9 +def not_okay_a(): + if True: + return None + else: + return +#: Okay +def okay_nested_a(): + def f(): + if 1: + return + else: + return + return f +#: Okay +def okay_nested_b(): + def f(): + if 1: + return 3 + else: + return 4 + return +#: E750:6:5 +def not_okay_nested_a(x): + def f(): + return + if not x: + return + return f +#: E750:6:13 +def not_okay_nested_b(): + def f(): + if 1: + return 3 + else: + return + return diff --git a/testsuite/support.py b/testsuite/support.py index cf9abc53..15adf463 100644 --- a/testsuite/support.py +++ b/testsuite/support.py @@ -38,7 +38,7 @@ def error(self, line_number, offset, text, check): detailed_code = '%s:%s:%s' % (code, line_number, offset + 1) # Don't care about expected errors or warnings if code in self.expected or detailed_code in self.expected: - return + return None self._deferred_print.append( (line_number, offset, detailed_code, text[5:], check.__doc__)) self.file_errors += 1