diff --git a/CHANGELOG.md b/CHANGELOG.md index a1d999d2..8dcf8fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # next (unreleased) +* Improve reachability analysis (kreathon, #270, #302). * Add type hints for `get_unused_code` and the fields of the `Item` class (John Doknjas, #361). # 2.13 (2024-10-02) diff --git a/tests/__init__.py b/tests/__init__.py index c54b3877..4167a4dc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -39,6 +39,14 @@ def check_unreachable(v, lineno, size, name): assert item.name == name +def check_multiple_unreachable(v, checks): + assert len(v.unreachable_code) == len(checks) + for item, (lineno, size, name) in zip(v.unreachable_code, checks): + assert item.first_lineno == lineno + assert item.size == size + assert item.name == name + + @pytest.fixture def v(): return core.Vulture(verbose=True) diff --git a/tests/test_conditions.py b/tests/test_conditions.py index 3799d442..c401b226 100644 --- a/tests/test_conditions.py +++ b/tests/test_conditions.py @@ -2,7 +2,6 @@ from vulture import utils -from . import check_unreachable from . import v assert v # Silence pyflakes @@ -73,147 +72,3 @@ def test_errors(): condition = ast.parse(condition, mode="eval").body assert not utils.condition_is_always_false(condition) assert not utils.condition_is_always_true(condition) - - -def test_while(v): - v.scan( - """\ -while False: - pass -""" - ) - check_unreachable(v, 1, 2, "while") - - -def test_while_nested(v): - v.scan( - """\ -while True: - while False: - pass -""" - ) - check_unreachable(v, 2, 2, "while") - - -def test_if_false(v): - v.scan( - """\ -if False: - pass -""" - ) - check_unreachable(v, 1, 2, "if") - - -def test_elif_false(v): - v.scan( - """\ -if bar(): - pass -elif False: - print("Unreachable") -""" - ) - check_unreachable(v, 3, 2, "if") - - -def test_nested_if_statements_false(v): - v.scan( - """\ -if foo(): - if bar(): - pass - elif False: - print("Unreachable") - pass - elif something(): - print("Reachable") - else: - pass -else: - pass -""" - ) - check_unreachable(v, 4, 3, "if") - - -def test_if_false_same_line(v): - v.scan( - """\ -if False: a = 1 -else: c = 3 -""" - ) - check_unreachable(v, 1, 1, "if") - - -def test_if_true(v): - v.scan( - """\ -if True: - a = 1 - b = 2 -else: - c = 3 - d = 3 -""" - ) - # For simplicity, we don't report the "else" line as dead code. - check_unreachable(v, 5, 2, "else") - - -def test_if_true_same_line(v): - v.scan( - """\ -if True: - a = 1 - b = 2 -else: c = 3 -d = 3 -""" - ) - check_unreachable(v, 4, 1, "else") - - -def test_nested_if_statements_true(v): - v.scan( - """\ -if foo(): - if bar(): - pass - elif True: - if something(): - pass - else: - pass - elif something_else(): - print("foo") - else: - print("bar") -else: - pass -""" - ) - check_unreachable(v, 9, 4, "else") - - -def test_redundant_if(v): - v.scan( - """\ -if [5]: - pass -""" - ) - print(v.unreachable_code[0].size) - check_unreachable(v, 1, 2, "if") - - -def test_if_exp_true(v): - v.scan("foo if True else bar") - check_unreachable(v, 1, 1, "ternary") - - -def test_if_exp_false(v): - v.scan("foo if False else bar") - check_unreachable(v, 1, 1, "ternary") diff --git a/tests/test_reachability.py b/tests/test_reachability.py new file mode 100644 index 00000000..a7eb24a3 --- /dev/null +++ b/tests/test_reachability.py @@ -0,0 +1,744 @@ +from . import check_multiple_unreachable, check_unreachable +from . import v + +assert v # Silence pyflakes + + +def test_return_assignment(v): + v.scan( + """\ +def foo(): + print("Hello World") + return + a = 1 +""" + ) + check_unreachable(v, 4, 1, "return") + + +def test_return_multiline_return_statements(v): + v.scan( + """\ +def foo(): + print("Something") + return (something, + that, + spans, + over, + multiple, + lines) + print("Hello World") +""" + ) + check_unreachable(v, 9, 1, "return") + + +def test_return_multiple_return_statements(v): + v.scan( + """\ +def foo(): + return something + return None + return (some, statement) +""" + ) + check_unreachable(v, 3, 2, "return") + + +def test_return_pass(v): + v.scan( + """\ +def foo(): + return + pass + return something +""" + ) + check_unreachable(v, 3, 2, "return") + + +def test_return_multiline_return(v): + v.scan( + """ +def foo(): + return \ + "Hello" + print("Unreachable code") +""" + ) + check_unreachable(v, 4, 1, "return") + + +def test_return_recursive_functions(v): + v.scan( + """\ +def foo(a): + if a == 1: + return 1 + else: + return foo(a - 1) + print("This line is never executed") +""" + ) + check_unreachable(v, 6, 1, "return") + + +def test_return_semicolon(v): + v.scan( + """\ +def foo(): + return; a = 1 +""" + ) + check_unreachable(v, 2, 1, "return") + + +def test_return_list(v): + v.scan( + """\ +def foo(a): + return + a[1:2] +""" + ) + check_unreachable(v, 3, 1, "return") + + +def test_return_continue(v): + v.scan( + """\ +def foo(): + if foo(): + return True + continue + else: + return False +""" + ) + check_unreachable(v, 4, 1, "return") + + +def test_return_function_definition(v): + v.scan( + """\ +def foo(): + return True + def bar(): + return False +""" + ) + check_unreachable(v, 3, 2, "return") + + +def test_raise_global(v): + v.scan( + """\ +raise ValueError +a = 1 +""" + ) + check_unreachable(v, 2, 1, "raise") + + +def test_raise_assignment(v): + v.scan( + """\ +def foo(): + raise ValueError + li = [] +""" + ) + check_unreachable(v, 3, 1, "raise") + + +def test_multiple_raise_statements(v): + v.scan( + """\ +def foo(): + a = 1 + raise + raise KeyError + # a comment + b = 2 + raise CustomDefinedError +""" + ) + check_unreachable(v, 4, 4, "raise") + + +def test_return_with_raise(v): + v.scan( + """\ +def foo(): + a = 1 + return + raise ValueError + return +""" + ) + check_unreachable(v, 4, 2, "return") + + +def test_return_comment_and_code(v): + v.scan( + """\ +def foo(): + return + # This is a comment + print("Hello World") +""" + ) + check_unreachable(v, 4, 1, "return") + + +def test_raise_with_return(v): + v.scan( + """\ +def foo(): + a = 1 + raise + return a +""" + ) + check_unreachable(v, 4, 1, "raise") + + +def test_raise_error_message(v): + v.scan( + """\ +def foo(): + raise SomeError("There is a problem") + print("I am unreachable") +""" + ) + check_unreachable(v, 3, 1, "raise") + + +def test_raise_try_except(v): + v.scan( + """\ +def foo(): + try: + a = 1 + raise + except IOError as e: + print("We have some problem.") + raise + print(":-(") +""" + ) + check_unreachable(v, 8, 1, "raise") + + +def test_raise_with_comment_and_code(v): + v.scan( + """\ +def foo(): + raise + # This is a comment + print("Something") + return None +""" + ) + check_unreachable(v, 4, 2, "raise") + + +def test_continue_basic(v): + v.scan( + """\ +def foo(): + if bar(): + a = 1 + else: + continue + a = 2 +""" + ) + check_unreachable(v, 6, 1, "continue") + + +def test_continue_one_liner(v): + v.scan( + """\ +def foo(): + for i in range(1, 10): + if i == 5: continue + print(1 / i) +""" + ) + assert v.unreachable_code == [] + + +def test_continue_nested_loops(v): + v.scan( + """\ +def foo(): + a = 0 + if something(): + foo() + if bar(): + a = 2 + continue + # This is unreachable + a = 1 + elif a == 1: + pass + else: + a = 3 + continue + else: + continue +""" + ) + check_unreachable(v, 9, 1, "continue") + + +def test_continue_with_comment_and_code(v): + v.scan( + """\ +def foo(): + if bar1(): + bar2() + else: + a = 1 + continue + # Just a comment + raise ValueError +""" + ) + check_unreachable(v, 8, 1, "continue") + + +def test_break_basic(v): + v.scan( + """\ +def foo(): + for i in range(123): + break + # A comment + return + dead = 1 +""" + ) + check_unreachable(v, 5, 2, "break") + + +def test_break_one_liner(v): + v.scan( + """\ +def foo(): + for i in range(10): + if i == 3: break + print(i) +""" + ) + assert v.unreachable_code == [] + + +def test_break_with_comment_and_code(v): + v.scan( + """\ +while True: + break + # some comment + print("Hello") +""" + ) + check_unreachable(v, 4, 1, "break") + + +def test_if_false(v): + v.scan( + """\ +if False: + pass +""" + ) + check_unreachable(v, 1, 2, "if") + + +def test_elif_false(v): + v.scan( + """\ +if bar(): + pass +elif False: + print("Unreachable") +""" + ) + check_unreachable(v, 3, 2, "if") + + +def test_nested_if_statements_false(v): + v.scan( + """\ +if foo(): + if bar(): + pass + elif False: + print("Unreachable") + pass + elif something(): + print("Reachable") + else: + pass +else: + pass +""" + ) + check_unreachable(v, 4, 3, "if") + + +def test_if_false_same_line(v): + v.scan( + """\ +if False: a = 1 +else: c = 3 +""" + ) + check_unreachable(v, 1, 1, "if") + + +def test_if_true(v): + v.scan( + """\ +if True: + a = 1 + b = 2 +else: + c = 3 + d = 3 +""" + ) + # For simplicity, we don't report the "else" line as dead code. + check_unreachable(v, 5, 2, "else") + + +def test_if_true_same_line(v): + v.scan( + """\ +if True: + a = 1 + b = 2 +else: c = 3 +d = 3 +""" + ) + check_unreachable(v, 4, 1, "else") + + +def test_nested_if_statements_true(v): + v.scan( + """\ +if foo(): + if bar(): + pass + elif True: + if something(): + pass + else: + pass + elif something_else(): + print("foo") + else: + print("bar") +else: + pass +""" + ) + check_unreachable(v, 9, 4, "else") + + +def test_redundant_if(v): + v.scan( + """\ +if [5]: + pass +""" + ) + print(v.unreachable_code[0].size) + check_unreachable(v, 1, 2, "if") + + +def test_if_exp_true(v): + v.scan("foo if True else bar") + check_unreachable(v, 1, 1, "ternary") + + +def test_if_exp_false(v): + v.scan("foo if False else bar") + check_unreachable(v, 1, 1, "ternary") + + +def test_if_true_return(v): + v.scan( + """\ +def foo(a): + if True: + return 0 + print(":-(") +""" + ) + check_multiple_unreachable(v, [(2, 2, "if"), (4, 1, "if")]) + + +def test_if_true_return_else(v): + v.scan( + """\ +def foo(a): + if True: + return 0 + else: + return 1 + print(":-(") +""" + ) + check_multiple_unreachable(v, [(5, 1, "else"), (6, 1, "if")]) + + +def test_if_some_branches_return(v): + v.scan( + """\ +def foo(a): + if a == 0: + return 0 + elif a == 1: + pass + else: + return 2 + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_if_all_branches_return(v): + v.scan( + """\ +def foo(a): + if a == 0: + return 0 + elif a == 1: + return 1 + else: + return 2 + print(":-(") +""" + ) + check_unreachable(v, 8, 1, "if") + + +def test_if_all_branches_return_nested(v): + v.scan( + """\ +def foo(a, b): + if a: + if b: + return 1 + return 2 + else: + return 3 + print(":-(") +""" + ) + check_unreachable(v, 8, 1, "if") + + +def test_if_all_branches_return_or_raise(v): + v.scan( + """\ +def foo(a): + if a == 0: + return 0 + else: + raise Exception() + print(":-(") +""" + ) + check_unreachable(v, 6, 1, "if") + + +def test_try_fall_through(v): + v.scan( + """\ +def foo(): + try: + pass + except IndexError as e: + raise e + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_try_some_branches_raise(v): + v.scan( + """\ +def foo(e): + try: + raise e + except IndexError as e: + pass + except Exception as e: + raise e + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_try_all_branches_return_or_raise(v): + v.scan( + """\ +def foo(): + try: + return 2 + except IndexError as e: + raise e + except Exception as e: + raise e + print(":-(") +""" + ) + check_unreachable(v, 8, 1, "try") + + +def test_try_nested_no_fall_through(v): + v.scan( + """\ +def foo(a): + try: + raise a + except: + try: + return + except Exception as e: + raise e + print(":-(") +""" + ) + check_unreachable(v, 9, 1, "try") + + +def test_try_reachable_else(v): + v.scan( + """\ +def foo(): + try: + print(":-)") + except: + return 1 + else: + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_try_unreachable_else(v): + v.scan( + """\ +def foo(): + try: + raise Exception() + except Exception as e: + return 1 + else: + print(":-(") +""" + ) + check_unreachable(v, 7, 1, "else") + + +def test_with_fall_through(v): + v.scan( + """\ +def foo(a): + with a(): + raise Exception() + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_async_with_fall_through(v): + v.scan( + """\ +async def foo(a): + async with a(): + raise Exception() + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_for_fall_through(v): + v.scan( + """\ +def foo(a): + for i in a: + raise Exception() + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_async_for_fall_through(v): + v.scan( + """\ +async def foo(a): + async for i in a: + raise Exception() + print(":-(") +""" + ) + assert v.unreachable_code == [] + + +def test_while_false(v): + v.scan( + """\ +while False: + pass +""" + ) + check_unreachable(v, 1, 2, "while") + + +def test_while_nested(v): + v.scan( + """\ +while True: + while False: + pass +""" + ) + check_unreachable(v, 2, 2, "while") + + +def test_while_true_else(v): + v.scan( + """\ +while True: + print("I won't stop") +else: + print("I won't run") +""" + ) + check_unreachable(v, 4, 1, "else") + + +def test_while_fall_through(v): + v.scan( + """\ +def foo(a): + while a > 0: + return 1 + print(":-(") +""" + ) + assert v.unreachable_code == [] diff --git a/tests/test_unreachable.py b/tests/test_unreachable.py deleted file mode 100644 index 11e81256..00000000 --- a/tests/test_unreachable.py +++ /dev/null @@ -1,337 +0,0 @@ -from . import check_unreachable -from . import v - -assert v # Silence pyflakes - - -def test_return_assignment(v): - v.scan( - """\ -def foo(): - print("Hello World") - return - a = 1 -""" - ) - check_unreachable(v, 4, 1, "return") - - -def test_return_multiline_return_statements(v): - v.scan( - """\ -def foo(): - print("Something") - return (something, - that, - spans, - over, - multiple, - lines) - print("Hello World") -""" - ) - check_unreachable(v, 9, 1, "return") - - -def test_return_multiple_return_statements(v): - v.scan( - """\ -def foo(): - return something - return None - return (some, statement) -""" - ) - check_unreachable(v, 3, 2, "return") - - -def test_return_pass(v): - v.scan( - """\ -def foo(): - return - pass - return something -""" - ) - check_unreachable(v, 3, 2, "return") - - -def test_return_multiline_return(v): - v.scan( - """ -def foo(): - return \ - "Hello" - print("Unreachable code") -""" - ) - check_unreachable(v, 4, 1, "return") - - -def test_return_recursive_functions(v): - v.scan( - """\ -def foo(a): - if a == 1: - return 1 - else: - return foo(a - 1) - print("This line is never executed") -""" - ) - check_unreachable(v, 6, 1, "return") - - -def test_return_semicolon(v): - v.scan( - """\ -def foo(): - return; a = 1 -""" - ) - check_unreachable(v, 2, 1, "return") - - -def test_return_list(v): - v.scan( - """\ -def foo(a): - return - a[1:2] -""" - ) - check_unreachable(v, 3, 1, "return") - - -def test_return_continue(v): - v.scan( - """\ -def foo(): - if foo(): - return True - continue - else: - return False -""" - ) - check_unreachable(v, 4, 1, "return") - - -def test_raise_assignment(v): - v.scan( - """\ -def foo(): - raise ValueError - li = [] -""" - ) - check_unreachable(v, 3, 1, "raise") - - -def test_multiple_raise_statements(v): - v.scan( - """\ -def foo(): - a = 1 - raise - raise KeyError - # a comment - b = 2 - raise CustomDefinedError -""" - ) - check_unreachable(v, 4, 4, "raise") - - -def test_return_with_raise(v): - v.scan( - """\ -def foo(): - a = 1 - return - raise ValueError - return -""" - ) - check_unreachable(v, 4, 2, "return") - - -def test_return_comment_and_code(v): - v.scan( - """\ -def foo(): - return - # This is a comment - print("Hello World") -""" - ) - check_unreachable(v, 4, 1, "return") - - -def test_raise_with_return(v): - v.scan( - """\ -def foo(): - a = 1 - raise - return a -""" - ) - check_unreachable(v, 4, 1, "raise") - - -def test_raise_error_message(v): - v.scan( - """\ -def foo(): - raise SomeError("There is a problem") - print("I am unreachable") -""" - ) - check_unreachable(v, 3, 1, "raise") - - -def test_raise_try_except(v): - v.scan( - """\ -def foo(): - try: - a = 1 - raise - except IOError as e: - print("We have some problem.") - raise - print(":-(") -""" - ) - check_unreachable(v, 8, 1, "raise") - - -def test_raise_with_comment_and_code(v): - v.scan( - """\ -def foo(): - raise - # This is a comment - print("Something") - return None -""" - ) - check_unreachable(v, 4, 2, "raise") - - -def test_continue_basic(v): - v.scan( - """\ -def foo(): - if bar(): - a = 1 - else: - continue - a = 2 -""" - ) - check_unreachable(v, 6, 1, "continue") - - -def test_continue_one_liner(v): - v.scan( - """\ -def foo(): - for i in range(1, 10): - if i == 5: continue - print(1 / i) -""" - ) - assert v.unreachable_code == [] - - -def test_continue_nested_loops(v): - v.scan( - """\ -def foo(): - a = 0 - if something(): - foo() - if bar(): - a = 2 - continue - # This is unreachable - a = 1 - elif a == 1: - pass - else: - a = 3 - continue - else: - continue -""" - ) - check_unreachable(v, 9, 1, "continue") - - -def test_continue_with_comment_and_code(v): - v.scan( - """\ -def foo(): - if bar1(): - bar2() - else: - a = 1 - continue - # Just a comment - raise ValueError -""" - ) - check_unreachable(v, 8, 1, "continue") - - -def test_break_basic(v): - v.scan( - """\ -def foo(): - for i in range(123): - break - # A comment - return - dead = 1 -""" - ) - check_unreachable(v, 5, 2, "break") - - -def test_break_one_liner(v): - v.scan( - """\ -def foo(): - for i in range(10): - if i == 3: break - print(i) -""" - ) - assert v.unreachable_code == [] - - -def test_break_with_comment_and_code(v): - v.scan( - """\ -while True: - break - # some comment - print("Hello") -""" - ) - check_unreachable(v, 4, 1, "break") - - -def test_while_true_else(v): - v.scan( - """\ -while True: - print("I won't stop") -else: - print("I won't run") -""" - ) - check_unreachable(v, 4, 1, "else") diff --git a/vulture/core.py b/vulture/core.py index 2cadabb6..5b7db43a 100644 --- a/vulture/core.py +++ b/vulture/core.py @@ -1,5 +1,6 @@ import ast from fnmatch import fnmatch, fnmatchcase +from functools import partial from pathlib import Path import pkgutil import re @@ -11,6 +12,7 @@ from vulture import noqa from vulture import utils from vulture.config import InputError, make_config +from vulture.reachability import Reachability from vulture.utils import ExitCode @@ -222,6 +224,13 @@ def get_list(typ): self.exit_code = ExitCode.NoDeadCode self.noqa_lines = {} + report = partial( + self._define, + collection=self.unreachable_code, + confidence=100, + ) + self.reachability = Reachability(report=report) + def scan(self, code, filename=""): filename = Path(filename) self.code = code.splitlines() @@ -258,6 +267,10 @@ def handle_syntax_error(e): except SyntaxError as err: handle_syntax_error(err) + # Reset the reachability internals for every module to reduce memory + # usage. + self.reachability.reset() + def scavenge(self, paths, exclude=None): def prepare_pattern(pattern): if not any(char in pattern for char in "*?["): @@ -417,47 +430,6 @@ def _add_aliases(self, node): if alias is not None: self.used_names.add(name_and_alias.name) - def _handle_conditional_node(self, node, name): - if utils.condition_is_always_false(node.test): - self._define( - self.unreachable_code, - name, - node, - last_node=node.body - if isinstance(node, ast.IfExp) - else node.body[-1], - message=f"unsatisfiable '{name}' condition", - confidence=100, - ) - elif utils.condition_is_always_true(node.test): - else_body = node.orelse - if name == "ternary": - self._define( - self.unreachable_code, - name, - else_body, - message="unreachable 'else' expression", - confidence=100, - ) - elif else_body: - self._define( - self.unreachable_code, - "else", - else_body[0], - last_node=else_body[-1], - message="unreachable 'else' block", - confidence=100, - ) - elif name == "if": - # Redundant if-condition without else block. - self._define( - self.unreachable_code, - name, - node, - message="redundant if-condition", - confidence=100, - ) - def _define( self, collection, @@ -630,12 +602,6 @@ def visit_FunctionDef(self, node): self.defined_funcs, node.name, node, ignore=_ignore_function ) - def visit_If(self, node): - self._handle_conditional_node(node, "if") - - def visit_IfExp(self, node): - self._handle_conditional_node(node, "ternary") - def visit_Import(self, node): self._add_aliases(node) @@ -659,14 +625,16 @@ def visit_Assign(self, node): if utils.is_ast_string(elt): self.used_names.add(elt.value) - def visit_While(self, node): - self._handle_conditional_node(node, "while") - def visit_MatchClass(self, node): for kwd_attr in node.kwd_attrs: self.used_names.add(kwd_attr) def visit(self, node): + # Visit children nodes first to allow recursive reachability analysis. + self.generic_visit(node) + + self.reachability.visit(node) + method = "visit_" + node.__class__.__name__ visitor = getattr(self, method, None) if self.verbose: @@ -689,36 +657,10 @@ def visit(self, node): ast.parse(type_comment, filename="", mode=mode) ) - return self.generic_visit(node) - - def _handle_ast_list(self, ast_list): - """ - Find unreachable nodes in the given sequence of ast nodes. - """ - for index, node in enumerate(ast_list): - if isinstance( - node, (ast.Break, ast.Continue, ast.Raise, ast.Return) - ): - try: - first_unreachable_node = ast_list[index + 1] - except IndexError: - continue - class_name = node.__class__.__name__.lower() - self._define( - self.unreachable_code, - class_name, - first_unreachable_node, - last_node=ast_list[-1], - message=f"unreachable code after '{class_name}'", - confidence=100, - ) - return - def generic_visit(self, node): """Called if no explicit visitor function exists for a node.""" for _, value in ast.iter_fields(node): if isinstance(value, list): - self._handle_ast_list(value) for item in value: if isinstance(item, ast.AST): self.visit(item) diff --git a/vulture/reachability.py b/vulture/reachability.py new file mode 100644 index 00000000..fc043828 --- /dev/null +++ b/vulture/reachability.py @@ -0,0 +1,193 @@ +import ast + +from vulture import utils + + +class Reachability: + def __init__(self, report): + self._report = report + self._no_fall_through_nodes = set() + + def visit(self, node): + """When called, all children of this node have already been visited.""" + if isinstance(node, (ast.Break, ast.Continue, ast.Return, ast.Raise)): + self._mark_as_no_fall_through(node) + elif isinstance( + node, + ( + ast.Module, + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.For, + ast.AsyncFor, + ast.With, + ast.AsyncWith, + ), + ): + self._can_fall_through_statements_analysis(node.body) + elif isinstance(node, ast.While): + self._handle_reachability_while(node) + elif isinstance(node, ast.If): + self._handle_reachability_if(node) + elif isinstance(node, ast.IfExp): + self._handle_reachability_if_expr(node) + elif isinstance(node, ast.Try): + self._handle_reachability_try(node) + + def reset(self): + self._no_fall_through_nodes = set() + + def _can_fall_through(self, node): + return node not in self._no_fall_through_nodes + + def _mark_as_no_fall_through(self, node): + self._no_fall_through_nodes.add(node) + + def _can_fall_through_statements_analysis(self, statements): + """Report unreachable statements. + Return True if we can execute the full list of statements. + """ + for idx, statement in enumerate(statements): + if not self._can_fall_through(statement): + try: + next_sibling = statements[idx + 1] + except IndexError: + next_sibling = None + if next_sibling is not None: + class_name = statement.__class__.__name__.lower() + self._report( + name=class_name, + first_node=next_sibling, + last_node=statements[-1], + message=f"unreachable code after '{class_name}'", + ) + return False + return True + + def _handle_reachability_if(self, node): + has_else = bool(node.orelse) + + if utils.condition_is_always_false(node.test): + self._report( + name="if", + first_node=node, + last_node=node.body + if isinstance(node, ast.IfExp) + else node.body[-1], + message="unsatisfiable 'if' condition", + ) + if_can_fall_through = True + else_can_fall_through = self._can_else_fall_through( + node.orelse, condition_always_true=False + ) + + elif utils.condition_is_always_true(node.test): + if_can_fall_through = self._can_fall_through_statements_analysis( + node.body + ) + else_can_fall_through = self._can_else_fall_through( + node.orelse, condition_always_true=True + ) + + if has_else: + self._report( + name="else", + first_node=node.orelse[0], + last_node=node.orelse[-1], + message="unreachable 'else' block", + ) + else: + # Redundant if-condition without else block. + self._report( + name="if", + first_node=node, + message="redundant if-condition", + ) + else: + if_can_fall_through = self._can_fall_through_statements_analysis( + node.body + ) + else_can_fall_through = self._can_else_fall_through( + node.orelse, condition_always_true=False + ) + + statement_can_fall_through = ( + if_can_fall_through or else_can_fall_through + ) + + if not statement_can_fall_through: + self._mark_as_no_fall_through(node) + + def _can_else_fall_through(self, orelse, condition_always_true): + if not orelse: + return not condition_always_true + return self._can_fall_through_statements_analysis(orelse) + + def _handle_reachability_if_expr(self, node): + if utils.condition_is_always_false(node.test): + self._report( + name="ternary", + first_node=node, + last_node=node.body + if isinstance(node, ast.IfExp) + else node.body[-1], + message="unsatisfiable 'ternary' condition", + ) + elif utils.condition_is_always_true(node.test): + else_body = node.orelse + self._report( + name="ternary", + first_node=else_body, + message="unreachable 'else' expression", + ) + + def _handle_reachability_while(self, node): + if utils.condition_is_always_false(node.test): + self._report( + name="while", + first_node=node, + last_node=node.body + if isinstance(node, ast.IfExp) + else node.body[-1], + message="unsatisfiable 'while' condition", + ) + + elif utils.condition_is_always_true(node.test): + else_body = node.orelse + if else_body: + self._report( + name="else", + first_node=else_body[0], + last_node=else_body[-1], + message="unreachable 'else' block", + ) + + self._can_fall_through_statements_analysis(node.body) + + def _handle_reachability_try(self, node): + try_can_fall_through = self._can_fall_through_statements_analysis( + node.body + ) + + has_else = bool(node.orelse) + + if not try_can_fall_through and has_else: + else_body = node.orelse + self._report( + name="else", + first_node=else_body[0], + last_node=else_body[-1], + message="unreachable 'else' block", + ) + + any_except_can_fall_through = any( + self._can_fall_through_statements_analysis(handler.body) + for handler in node.handlers + ) + + statement_can_fall_through = ( + try_can_fall_through or any_except_can_fall_through + ) + + if not statement_can_fall_through: + self._mark_as_no_fall_through(node)