From 8f6812597216bb4f664e7203795d837f766111e2 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 11 Apr 2024 17:20:11 -0400 Subject: [PATCH] Revert "Remove `unreachable-code` feature (#9463)" This reverts commit f9dd7bb190e0fd4d85ab1f908ae4c2bb8a1ffef0. --- .../resources/test/fixtures/ruff/RUF014.py | 185 +++ .../src/checkers/ast/analyze/statement.rs | 5 + crates/ruff_linter/src/codes.rs | 2 + crates/ruff_linter/src/rules/ruff/mod.rs | 4 + .../ruff_linter/src/rules/ruff/rules/mod.rs | 4 + ...les__unreachable__tests__assert.py.md.snap | 97 ++ ...__unreachable__tests__async-for.py.md.snap | 241 ++++ ..._rules__unreachable__tests__for.py.md.snap | 302 +++++ ...__rules__unreachable__tests__if.py.md.snap | 553 ++++++++ ...ules__unreachable__tests__match.py.md.snap | 815 ++++++++++++ ...ules__unreachable__tests__raise.py.md.snap | 41 + ...les__unreachable__tests__simple.py.md.snap | 136 ++ ...ules__unreachable__tests__while.py.md.snap | 527 ++++++++ .../src/rules/ruff/rules/unreachable.rs | 1114 +++++++++++++++++ ..._rules__ruff__tests__RUF014_RUF014.py.snap | 249 ++++ 15 files changed, 4275 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF014.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__assert.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__async-for.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__for.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__if.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__match.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__raise.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__simple.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__while.py.md.snap create mode 100644 crates/ruff_linter/src/rules/ruff/rules/unreachable.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF014_RUF014.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF014.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF014.py new file mode 100644 index 00000000000000..d1ae40f3ca8648 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF014.py @@ -0,0 +1,185 @@ +def after_return(): + return "reachable" + return "unreachable" + +async def also_works_on_async_functions(): + return "reachable" + return "unreachable" + +def if_always_true(): + if True: + return "reachable" + return "unreachable" + +def if_always_false(): + if False: + return "unreachable" + return "reachable" + +def if_elif_always_false(): + if False: + return "unreachable" + elif False: + return "also unreachable" + return "reachable" + +def if_elif_always_true(): + if False: + return "unreachable" + elif True: + return "reachable" + return "also unreachable" + +def ends_with_if(): + if False: + return "unreachable" + else: + return "reachable" + +def infinite_loop(): + while True: + continue + return "unreachable" + +''' TODO: we could determine these, but we don't yet. +def for_range_return(): + for i in range(10): + if i == 5: + return "reachable" + return "unreachable" + +def for_range_else(): + for i in range(111): + if i == 5: + return "reachable" + else: + return "unreachable" + return "also unreachable" + +def for_range_break(): + for i in range(13): + return "reachable" + return "unreachable" + +def for_range_if_break(): + for i in range(1110): + if True: + return "reachable" + return "unreachable" +''' + +def match_wildcard(status): + match status: + case _: + return "reachable" + return "unreachable" + +def match_case_and_wildcard(status): + match status: + case 1: + return "reachable" + case _: + return "reachable" + return "unreachable" + +def raise_exception(): + raise Exception + return "unreachable" + +def while_false(): + while False: + return "unreachable" + return "reachable" + +def while_false_else(): + while False: + return "unreachable" + else: + return "reachable" + +def while_false_else_return(): + while False: + return "unreachable" + else: + return "reachable" + return "also unreachable" + +def while_true(): + while True: + return "reachable" + return "unreachable" + +def while_true_else(): + while True: + return "reachable" + else: + return "unreachable" + +def while_true_else_return(): + while True: + return "reachable" + else: + return "unreachable" + return "also unreachable" + +def while_false_var_i(): + i = 0 + while False: + i += 1 + return i + +def while_true_var_i(): + i = 0 + while True: + i += 1 + return i + +def while_infinite(): + while True: + pass + return "unreachable" + +def while_if_true(): + while True: + if True: + return "reachable" + return "unreachable" + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh1(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data + +''' +TODO: because `try` statements aren't handled this triggers a false positive as +the last statement is reached, but the rules thinks it isn't (it doesn't +see/process the break statement). + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None: + self.stop_serving = False + while True: + try: + self.server = HTTPServer((host, port), HtmlOnlyHandler) + self.host = host + self.port = port + break + except OSError: + log.debug(f"port {port} is in use, trying to next one") + port += 1 + + self.thread = threading.Thread(target=self._run_web_server) +''' diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index e3ca7a6d11b77d..4b4f7e9e4697cf 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -342,6 +342,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::TrioAsyncFunctionWithTimeout) { flake8_trio::rules::async_function_with_timeout(checker, function_def); } + if checker.enabled(Rule::UnreachableCode) { + checker + .diagnostics + .extend(ruff::rules::unreachable::in_function(name, body)); + } if checker.enabled(Rule::ReimplementedOperator) { refurb::rules::reimplemented_operator(checker, &function_def.into()); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index dae1651b7893e5..07d3968c73ccae 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -949,6 +949,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "011") => (RuleGroup::Removed, rules::ruff::rules::RuffStaticKeyDictComprehension), (Ruff, "012") => (RuleGroup::Stable, rules::ruff::rules::MutableClassDefault), (Ruff, "013") => (RuleGroup::Stable, rules::ruff::rules::ImplicitOptional), + #[allow(deprecated)] + (Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode), (Ruff, "015") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement), (Ruff, "016") => (RuleGroup::Stable, rules::ruff::rules::InvalidIndexType), (Ruff, "017") => (RuleGroup::Stable, rules::ruff::rules::QuadraticListSummation), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 6d2d76f544fbeb..cf58f78ea9c6e5 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -38,6 +38,10 @@ mod tests { Path::new("RUF015.py") )] #[test_case(Rule::InvalidIndexType, Path::new("RUF016.py"))] + #[cfg_attr( + feature = "unreachable-code", + test_case(Rule::UnreachableCode, Path::new("RUF014.py")) + )] #[test_case(Rule::QuadraticListSummation, Path::new("RUF017_1.py"))] #[test_case(Rule::QuadraticListSummation, Path::new("RUF017_0.py"))] #[test_case(Rule::AssignmentInAssert, Path::new("RUF018.py"))] diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index d4461f1f3b6aff..d6bb4997bcab50 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -25,6 +25,8 @@ pub(crate) use test_rules::*; pub(crate) use unnecessary_dict_comprehension_for_iterable::*; pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_key_check::*; +#[cfg(feature = "unreachable-code")] +pub(crate) use unreachable::*; pub(crate) use unused_noqa::*; mod ambiguous_unicode_character; @@ -58,6 +60,8 @@ pub(crate) mod test_rules; mod unnecessary_dict_comprehension_for_iterable; mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_key_check; +#[cfg(feature = "unreachable-code")] +pub(crate) mod unreachable; mod unused_noqa; #[derive(Clone, Copy)] diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__assert.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__assert.py.md.snap new file mode 100644 index 00000000000000..50dcab6daa13bf --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__assert.py.md.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + assert True +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + assert False +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + assert True, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + assert False, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__async-for.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__async-for.py.md.snap new file mode 100644 index 00000000000000..2847a7d87366bb --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__async-for.py.md.snap @@ -0,0 +1,241 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + async for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["async for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + async for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["async for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + async for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["async for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + async for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["async for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + async for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["async for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + async for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["async for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + async for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["async for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + async for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["async for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__for.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__for.py.md.snap new file mode 100644 index 00000000000000..6850c5f69b04be --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__for.py.md.snap @@ -0,0 +1,302 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + for i in range(5): + pass + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + block1["return 1\n"] + block2["for i in range(5): + pass + else: + return 1\n"] + + start --> block2 + block2 -- "range(5)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + for i in range(5): + pass + else: + return 1 + x = 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["x = 1\n"] + block1["pass\n"] + block2["return 1\n"] + block3["for i in range(5): + pass + else: + return 1\n"] + + start --> block3 + block3 -- "range(5)" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> block3 + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__if.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__if.py.md.snap new file mode 100644 index 00000000000000..6899a857742602 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__if.py.md.snap @@ -0,0 +1,553 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + if False: + return 0 + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if False: + return 0 + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if True: + return 1 + else: + return 0\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["return 1\n"] + block3["if False: + return 0 + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["return 0\n"] + block3["if True: + return 1 + else: + return 0\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + if True: + if True: + return 1 + return 2 + else: + return 3 + return "unreachable2" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable2#quot;\n"] + block1["return 2\n"] + block2["return 1\n"] + block3["if True: + return 1\n"] + block4["return 3\n"] + block5["if True: + if True: + return 1 + return 2 + else: + return 3\n"] + + start --> block5 + block5 -- "True" --> block3 + block5 -- "else" --> block4 + block4 --> return + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + if False: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + if True: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + if True: + return 1 + elif False: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["return 2\n"] + block3["if True: + return 1 + elif False: + return 2 + else: + return 0\n"] + block4["if True: + return 1 + elif False: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "True" --> block0 + block4 -- "else" --> block3 + block3 -- "False" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + if False: + return 1 + elif True: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["return 2\n"] + block3["if False: + return 1 + elif True: + return 2 + else: + return 0\n"] + block4["if False: + return 1 + elif True: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "False" --> block0 + block4 -- "else" --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 11 +### Source +```python +def func(): + if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5 + return 6 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 3\n"] + block2["return 0\n"] + block3["return 2\n"] + block4["return 1\n"] + block5["if False: + return 0 + elif True: + return 1 + else: + return 2\n"] + block6["if False: + return 0 + elif True: + return 1 + else: + return 2\n"] + block7["return 5\n"] + block8["return 4\n"] + block9["if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5\n"] + block10["if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5\n"] + + start --> block10 + block10 -- "True" --> block6 + block10 -- "else" --> block9 + block9 -- "True" --> block8 + block9 -- "else" --> block7 + block8 --> return + block7 --> return + block6 -- "False" --> block2 + block6 -- "else" --> block5 + block5 -- "True" --> block4 + block5 -- "else" --> block3 + block4 --> return + block3 --> return + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + if False: + return "unreached" + elif False: + return "also unreached" + return "reached" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;reached#quot;\n"] + block1["return #quot;unreached#quot;\n"] + block2["return #quot;also unreached#quot;\n"] + block3["if False: + return #quot;unreached#quot; + elif False: + return #quot;also unreached#quot;\n"] + block4["if False: + return #quot;unreached#quot; + elif False: + return #quot;also unreached#quot;\n"] + + start --> block4 + block4 -- "False" --> block1 + block4 -- "else" --> block3 + block3 -- "False" --> block2 + block3 -- "else" --> block0 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 13 +### Source +```python +def func(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return buffer.data\n"] + block1["return base64.b64decode(data)\n"] + block2["buffer = self._buffers[id]\n"] + block3["self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block4["id = data[#quot;id#quot;]\nif id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block5["buffer = data\n"] + block6["if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block7["data = obj[#quot;data#quot;]\nif isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + + start --> block7 + block7 -- "isinstance(data, str)" --> block1 + block7 -- "else" --> block6 + block6 -- "isinstance(data, Buffer)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "id in self._buffers" --> block2 + block4 -- "else" --> block3 + block3 --> block0 + block2 --> block0 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__match.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__match.py.md.snap new file mode 100644 index 00000000000000..d74b08116f9361 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__match.py.md.snap @@ -0,0 +1,815 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(status): + match status: + case _: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["match status: + case _: + return 0\n"] + + start --> block2 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(status): + match status: + case 1: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["match status: + case 1: + return 1\n"] + + start --> block2 + block2 -- "case 1" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(status): + match status: + case 1: + return 1 + case _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["match status: + case 1: + return 1 + case _: + return 0\n"] + block2["return 1\n"] + block3["match status: + case 1: + return 1 + case _: + return 0\n"] + + start --> block3 + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + return 6 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 5\n"] + block2["match status: + case 1 | 2 | 3: + return 5\n"] + + start --> block2 + block2 -- "case 1 | 2 | 3" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + case _: + return 10 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 10\n"] + block2["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + block3["return 5\n"] + block4["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + + start --> block4 + block4 -- "case 1 | 2 | 3" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(status): + match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return "1 again" + case _: + return 3 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 3\n"] + block1["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block2["return #quot;1 again#quot;\n"] + block3["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block4["return 1\n"] + block5["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block6["return 0\n"] + block7["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + + start --> block7 + block7 -- "case 0" --> block6 + block7 -- "else" --> block5 + block6 --> return + block5 -- "case 1" --> block4 + block5 -- "else" --> block3 + block4 --> return + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, _: + return 0\n"] + block3["i = 0\n"] + + start --> block3 + block3 --> block2 + block2 -- "case _, _" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, 0: + return 0 + case _, 2: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block3["return 0\n"] + block4["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block5["i = 0\n"] + + start --> block5 + block5 --> block4 + block4 -- "case _, 0" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 -- "case _, 2" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case _: + raise ValueError("oops") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;oops#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + block3["print(#quot;Origin#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + + start --> block4 + block4 -- "case (0, 0)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case (0, y): + print(f"Y={y}") + case (x, 0): + print(f"X={x}") + case (x, y): + print(f"X={x}, Y={y}") + case _: + raise ValueError("Not a point") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;Not a point#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block3["print(f#quot;X={x}, Y={y}#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + + start --> block10 + block10 -- "case (0, 0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case (0, y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case (x, 0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case (x, y)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +def where_is(point): + class Point: + x: int + y: int + + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Not a point#quot;)\n"] + block2["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block3["print(#quot;Somewhere else#quot;)\n"] + block4["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block11["class Point: + x: int + y: int\n"] + + start --> block11 + block11 --> block10 + block10 -- "case Point(x=0, y=0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case Point(x=0, y=y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case Point(x=x, y=0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Point()" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 11 +### Source +```python +def func(points): + match points: + case []: + print("No points") + case [Point(0, 0)]: + print("The origin") + case [Point(x, y)]: + print(f"Single point {x}, {y}") + case [Point(0, y1), Point(0, y2)]: + print(f"Two on the Y axis at {y1}, {y2}") + case _: + print("Something else") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Something else#quot;)\n"] + block2["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block3["print(f#quot;Two on the Y axis at {y1}, {y2}#quot;)\n"] + block4["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block5["print(f#quot;Single point {x}, {y}#quot;)\n"] + block6["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block7["print(#quot;The origin#quot;)\n"] + block8["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block9["print(#quot;No points#quot;)\n"] + block10["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + + start --> block10 + block10 -- "case []" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case [Point(0, 0)]" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case [Point(x, y)]" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case [Point(0, y1), Point(0, y2)]" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 12 +### Source +```python +def func(point): + match point: + case Point(x, y) if x == y: + print(f"Y=X at {x}") + case Point(x, y): + print(f"Not on the diagonal") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(f#quot;Not on the diagonal#quot;)\n"] + block2["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + block3["print(f#quot;Y=X at {x}#quot;)\n"] + block4["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + + start --> block4 + block4 -- "case Point(x, y) if x == y" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Point(x, y)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 13 +### Source +```python +def func(): + from enum import Enum + class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue' + + color = Color(input("Enter your choice of 'red', 'blue' or 'green': ")) + + match color: + case Color.RED: + print("I see red!") + case Color.GREEN: + print("Grass is green") + case Color.BLUE: + print("I'm feeling the blues :(") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;I'm feeling the blues :(#quot;)\n"] + block2["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block3["print(#quot;Grass is green#quot;)\n"] + block4["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block5["print(#quot;I see red!#quot;)\n"] + block6["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block7["from enum import Enum\nclass Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue'\ncolor = Color(input(#quot;Enter your choice of 'red', 'blue' or 'green': #quot;))\n"] + + start --> block7 + block7 --> block6 + block6 -- "case Color.RED" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Color.GREEN" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Color.BLUE" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 14 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case foo: + raise ValueError("oops") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;oops#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case foo: + raise ValueError(#quot;oops#quot;)\n"] + block3["print(#quot;Origin#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case foo: + raise ValueError(#quot;oops#quot;)\n"] + + start --> block4 + block4 -- "case (0, 0)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__raise.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__raise.py.md.snap new file mode 100644 index 00000000000000..d0265fa6986793 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__raise.py.md.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + raise Exception +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise Exception\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + raise "a glass!" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise #quot;a glass!#quot;\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__simple.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__simple.py.md.snap new file mode 100644 index 00000000000000..015cf43dcc54fd --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__simple.py.md.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + return +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return\n"] + + start --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + + start --> block0 + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + + start --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + i = 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\n"] + + start --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + i += 2 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\ni += 2\nreturn i\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__while.py.md.snap b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__while.py.md.snap new file mode 100644 index 00000000000000..3491d2b7e22f95 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/snapshots/ruff_linter__rules__ruff__rules__unreachable__tests__while.py.md.snap @@ -0,0 +1,527 @@ +--- +source: crates/ruff_linter/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + while False: + return "unreachable" + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while False: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return #quot;unreachable#quot;\n"] + block2["return 1\n"] + block3["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + while True: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return 1\n"] + block2["return #quot;unreachable#quot;\n"] + block3["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + while False: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile False: + i += 1\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + i = 0 + while True: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile True: + i += 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + while True: + pass + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["pass\n"] + block2["while True: + pass\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + i = 0 + while True: + if True: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if True: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if True: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + i = 0 + while True: + if False: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if False: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if False: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "False" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 11 +### Source +```python +def func(): + while True: + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + block3["while True: + if True: + return 1\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + while True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while True: + continue\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 13 +### Source +```python +def func(): + while False: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while False: + continue\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 14 +### Source +```python +def func(): + while True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while True: + break\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 15 +### Source +```python +def func(): + while False: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while False: + break\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 16 +### Source +```python +def func(): + while True: + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["while True: + if True: + continue\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 17 +### Source +```python +def func(): + while True: + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["while True: + if True: + break\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff_linter/src/rules/ruff/rules/unreachable.rs b/crates/ruff_linter/src/rules/ruff/rules/unreachable.rs new file mode 100644 index 00000000000000..85040168488274 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/unreachable.rs @@ -0,0 +1,1114 @@ +use std::{fmt, iter, usize}; + +use log::error; +use ruff_python_ast::{ + Expr, ExprBooleanLiteral, Identifier, MatchCase, Pattern, PatternMatchAs, PatternMatchOr, Stmt, + StmtFor, StmtMatch, StmtReturn, StmtTry, StmtWhile, StmtWith, +}; +use ruff_text_size::{Ranged, TextRange, TextSize}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_index::{IndexSlice, IndexVec}; +use ruff_macros::{derive_message_formats, newtype_index, violation}; + +/// ## What it does +/// Checks for unreachable code. +/// +/// ## Why is this bad? +/// Unreachable code can be a maintenance burden without ever being used. +/// +/// ## Example +/// ```python +/// def function(): +/// if False: +/// return "unreachable" +/// return "reachable" +/// ``` +/// +/// Use instead: +/// ```python +/// def function(): +/// return "reachable" +/// ``` +#[violation] +pub struct UnreachableCode { + name: String, +} + +impl Violation for UnreachableCode { + #[derive_message_formats] + fn message(&self) -> String { + let UnreachableCode { name } = self; + format!("Unreachable code in {name}") + } +} + +pub(crate) fn in_function(name: &Identifier, body: &[Stmt]) -> Vec { + // Create basic code blocks from the body. + let basic_blocks = BasicBlocks::from(body); + + // Basic on the code blocks we can (more) easily follow what statements are + // and aren't reached, we'll mark them as such in `reached_map`. + let mut reached_map = Bitmap::with_capacity(basic_blocks.len()); + + if let Some(start_index) = basic_blocks.start_index() { + mark_reached(&mut reached_map, &basic_blocks.blocks, start_index); + } + + // For each unreached code block create a diagnostic. + reached_map + .unset() + .filter_map(|idx| { + let block = &basic_blocks.blocks[idx]; + if block.is_sentinel() { + return None; + } + + // TODO: add more information to the diagnostic. Include the entire + // code block, not just the first line. Maybe something to indicate + // the code flow and where it prevents this block from being reached + // for example. + let Some(stmt) = block.stmts.first() else { + // This should never happen. + error!("Got an unexpected empty code block"); + return None; + }; + Some(Diagnostic::new( + UnreachableCode { + name: name.as_str().to_owned(), + }, + stmt.range(), + )) + }) + .collect() +} + +/// Simple bitmap. +#[derive(Debug)] +struct Bitmap { + bits: Box<[usize]>, + capacity: usize, +} + +impl Bitmap { + /// Create a new `Bitmap` with `capacity` capacity. + fn with_capacity(capacity: usize) -> Bitmap { + let mut size = capacity / usize::BITS as usize; + if (capacity % usize::BITS as usize) != 0 { + size += 1; + } + Bitmap { + bits: vec![0; size].into_boxed_slice(), + capacity, + } + } + + /// Set bit at index `idx` to true. + /// + /// Returns a boolean indicating if the bit was already set. + fn set(&mut self, idx: BlockIndex) -> bool { + let bits_index = (idx.as_u32() / usize::BITS) as usize; + let shift = idx.as_u32() % usize::BITS; + if (self.bits[bits_index] & (1 << shift)) == 0 { + self.bits[bits_index] |= 1 << shift; + false + } else { + true + } + } + + /// Returns an iterator of all unset indices. + fn unset(&self) -> impl Iterator + '_ { + let mut index = 0; + let mut shift = 0; + let last_max_shift = self.capacity % usize::BITS as usize; + iter::from_fn(move || loop { + if shift >= usize::BITS as usize { + shift = 0; + index += 1; + } + if self.bits.len() <= index || (index >= self.bits.len() - 1 && shift >= last_max_shift) + { + return None; + } + + let is_set = (self.bits[index] & (1 << shift)) != 0; + shift += 1; + if !is_set { + return Some(BlockIndex::from_usize( + (index * usize::BITS as usize) + shift - 1, + )); + } + }) + } +} + +/// Set bits in `reached_map` for all blocks that are reached in `blocks` +/// starting with block at index `idx`. +fn mark_reached( + reached_map: &mut Bitmap, + blocks: &IndexSlice>, + start_index: BlockIndex, +) { + let mut idx = start_index; + + loop { + let block = &blocks[idx]; + if reached_map.set(idx) { + return; // Block already visited, no needed to do it again. + } + + match &block.next { + NextBlock::Always(next) => idx = *next, + NextBlock::If { + condition, + next, + orelse, + } => { + match taken(condition) { + Some(true) => idx = *next, // Always taken. + Some(false) => idx = *orelse, // Never taken. + None => { + // Don't know, both branches might be taken. + idx = *next; + mark_reached(reached_map, blocks, *orelse); + } + } + } + NextBlock::Terminate => return, + } + } +} + +/// Determines if `condition` is taken. +/// Returns `Some(true)` if the condition is always true, e.g. `if True`, same +/// with `Some(false)` if it's never taken. If it can't be determined it returns +/// `None`, e.g. `If i == 100`. +fn taken(condition: &Condition) -> Option { + // TODO: add more cases to this where we can determine a condition + // statically. For now we only consider constant booleans. + match condition { + Condition::Test(expr) => match expr { + Expr::BooleanLiteral(ExprBooleanLiteral { value, .. }) => Some(*value), + _ => None, + }, + Condition::Iterator(_) => None, + Condition::Match { .. } => None, + } +} + +/// Index into [`BasicBlocks::blocks`]. +#[newtype_index] +#[derive(PartialOrd, Ord)] +struct BlockIndex; + +/// Collection of basic block. +#[derive(Debug, PartialEq)] +struct BasicBlocks<'stmt> { + /// # Notes + /// + /// The order of these block is unspecified. However it's guaranteed that + /// the last block is the first statement in the function and the first + /// block is the last statement. The block are more or less in reverse + /// order, but it gets fussy around control flow statements (e.g. `while` + /// statements). + /// + /// For loop blocks, and similar recurring control flows, the end of the + /// body will point to the loop block again (to create the loop). However an + /// oddity here is that this block might contain statements before the loop + /// itself which, of course, won't be executed again. + /// + /// For example: + /// ```python + /// i = 0 # block 0 + /// while True: # + /// continue # block 1 + /// ``` + /// Will create a connection between block 1 (loop body) and block 0, which + /// includes the `i = 0` statement. + /// + /// To keep `NextBlock` simple(r) `NextBlock::If`'s `next` and `orelse` + /// fields only use `BlockIndex`, which means that they can't terminate + /// themselves. To support this we insert *empty*/fake blocks before the end + /// of the function that we can link to. + /// + /// Finally `BasicBlock` can also be a sentinel node, see the associated + /// constants of [`BasicBlock`]. + blocks: IndexVec>, +} + +impl BasicBlocks<'_> { + fn len(&self) -> usize { + self.blocks.len() + } + + fn start_index(&self) -> Option { + self.blocks.indices().last() + } +} + +impl<'stmt> From<&'stmt [Stmt]> for BasicBlocks<'stmt> { + /// # Notes + /// + /// This assumes that `stmts` is a function body. + fn from(stmts: &'stmt [Stmt]) -> BasicBlocks<'stmt> { + let mut blocks = BasicBlocksBuilder::with_capacity(stmts.len()); + + blocks.create_blocks(stmts, None); + + blocks.finish() + } +} + +/// Basic code block, sequence of statements unconditionally executed +/// "together". +#[derive(Debug, PartialEq)] +struct BasicBlock<'stmt> { + stmts: &'stmt [Stmt], + next: NextBlock<'stmt>, +} + +/// Edge between basic blocks (in the control-flow graph). +#[derive(Debug, PartialEq)] +enum NextBlock<'stmt> { + /// Always continue with a block. + Always(BlockIndex), + /// Condition jump. + If { + /// Condition that needs to be evaluated to jump to the `next` or + /// `orelse` block. + condition: Condition<'stmt>, + /// Next block if `condition` is true. + next: BlockIndex, + /// Next block if `condition` is false. + orelse: BlockIndex, + }, + /// The end. + Terminate, +} + +/// Condition used to determine to take the `next` or `orelse` branch in +/// [`NextBlock::If`]. +#[derive(Clone, Debug, PartialEq)] +enum Condition<'stmt> { + /// Conditional statement, this should evaluate to a boolean, for e.g. `if` + /// or `while`. + Test(&'stmt Expr), + /// Iterator for `for` statements, e.g. for `i in range(10)` this will be + /// `range(10)`. + Iterator(&'stmt Expr), + Match { + /// `match $subject`. + subject: &'stmt Expr, + /// `case $case`, include pattern, guard, etc. + case: &'stmt MatchCase, + }, +} + +impl<'stmt> Ranged for Condition<'stmt> { + fn range(&self) -> TextRange { + match self { + Condition::Test(expr) | Condition::Iterator(expr) => expr.range(), + // The case of the match statement, without the body. + Condition::Match { subject: _, case } => TextRange::new( + case.start(), + case.guard + .as_ref() + .map_or(case.pattern.end(), |guard| guard.end()), + ), + } + } +} + +impl<'stmt> BasicBlock<'stmt> { + /// A sentinel block indicating an empty termination block. + const EMPTY: BasicBlock<'static> = BasicBlock { + stmts: &[], + next: NextBlock::Terminate, + }; + + /// A sentinel block indicating an exception was raised. + const EXCEPTION: BasicBlock<'static> = BasicBlock { + stmts: &[Stmt::Return(StmtReturn { + range: TextRange::new(TextSize::new(0), TextSize::new(0)), + value: None, + })], + next: NextBlock::Terminate, + }; + + /// Return true if the block is a sentinel or fake block. + fn is_sentinel(&self) -> bool { + self.is_empty() || self.is_exception() + } + + /// Returns an empty block that terminates. + fn is_empty(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && self.stmts.is_empty() + } + + /// Returns true if `self` an [`BasicBlock::EXCEPTION`]. + fn is_exception(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && BasicBlock::EXCEPTION.stmts == self.stmts + } +} + +/// Handle a loop block, such as a `while`, `for` or `async for` statement. +fn loop_block<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + condition: Condition<'stmt>, + body: &'stmt [Stmt], + orelse: &'stmt [Stmt], + after: Option, +) -> NextBlock<'stmt> { + let after_block = blocks.maybe_next_block_index(after, || orelse.is_empty()); + // NOTE: a while loop's body must not be empty, so we can safely + // create at least one block from it. + let last_statement_index = blocks.append_blocks(body, after); + let last_orelse_statement = blocks.append_blocks_if_not_empty(orelse, after_block); + // `create_blocks` always continues to the next block by + // default. However in a while loop we want to continue with the + // while block (we're about to create) to create the loop. + // NOTE: `blocks.len()` is an invalid index at time of creation + // as it points to the block which we're about to create. + blocks.change_next_block( + last_statement_index, + after_block, + blocks.blocks.next_index(), + |block| { + // For `break` statements we don't want to continue with the + // loop, but instead with the statement after the loop (i.e. + // not change anything). + !block.stmts.last().is_some_and(Stmt::is_break_stmt) + }, + ); + NextBlock::If { + condition, + next: last_statement_index, + orelse: last_orelse_statement, + } +} + +/// Handle a single match case. +/// +/// `next_after_block` is the block *after* the entire match statement that is +/// taken after this case is taken. +/// `orelse_after_block` is the next match case (or the block after the match +/// statement if this is the last case). +fn match_case<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + match_stmt: &'stmt Stmt, + subject: &'stmt Expr, + case: &'stmt MatchCase, + next_after_block: BlockIndex, + orelse_after_block: BlockIndex, +) -> BasicBlock<'stmt> { + // FIXME: this is not ideal, we want to only use the `case` statement here, + // but that is type `MatchCase`, not `Stmt`. For now we'll point to the + // entire match statement. + let stmts = std::slice::from_ref(match_stmt); + let next_block_index = if case.body.is_empty() { + next_after_block + } else { + let from = blocks.last_index(); + let last_statement_index = blocks.append_blocks(&case.body, Some(next_after_block)); + if let Some(from) = from { + blocks.change_next_block(last_statement_index, from, next_after_block, |_| true); + } + last_statement_index + }; + let next = if is_wildcard(case) { + // Wildcard case is always taken. + NextBlock::Always(next_block_index) + } else { + NextBlock::If { + condition: Condition::Match { subject, case }, + next: next_block_index, + orelse: orelse_after_block, + } + }; + BasicBlock { stmts, next } +} + +/// Returns true if the [`MatchCase`] is a wildcard pattern. +fn is_wildcard(pattern: &MatchCase) -> bool { + /// Returns true if the [`Pattern`] is a wildcard pattern. + fn is_wildcard_pattern(pattern: &Pattern) -> bool { + match pattern { + Pattern::MatchValue(_) + | Pattern::MatchSingleton(_) + | Pattern::MatchSequence(_) + | Pattern::MatchMapping(_) + | Pattern::MatchClass(_) + | Pattern::MatchStar(_) => false, + Pattern::MatchAs(PatternMatchAs { pattern, .. }) => pattern.is_none(), + Pattern::MatchOr(PatternMatchOr { patterns, .. }) => { + patterns.iter().all(is_wildcard_pattern) + } + } + } + + pattern.guard.is_none() && is_wildcard_pattern(&pattern.pattern) +} + +#[derive(Debug, Default)] +struct BasicBlocksBuilder<'stmt> { + blocks: IndexVec>, +} + +impl<'stmt> BasicBlocksBuilder<'stmt> { + fn with_capacity(capacity: usize) -> Self { + Self { + blocks: IndexVec::with_capacity(capacity), + } + } + + /// Creates basic blocks from `stmts` and appends them to `blocks`. + fn create_blocks( + &mut self, + stmts: &'stmt [Stmt], + mut after: Option, + ) -> Option { + // We process the statements in reverse so that we can always point to the + // next block (as that should always be processed). + let mut stmts_iter = stmts.iter().enumerate().rev().peekable(); + while let Some((i, stmt)) = stmts_iter.next() { + let next = match stmt { + // Statements that continue to the next statement after execution. + Stmt::FunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Break(_) + | Stmt::TypeAlias(_) + | Stmt::IpyEscapeCommand(_) + | Stmt::Pass(_) => self.unconditional_next_block(after), + Stmt::Continue(_) => { + // NOTE: the next branch gets fixed up in `change_next_block`. + self.unconditional_next_block(after) + } + // Statements that (can) divert the control flow. + Stmt::If(stmt_if) => { + let after_consequent_block = + self.maybe_next_block_index(after, || needs_next_block(&stmt_if.body)); + let after_alternate_block = self.maybe_next_block_index(after, || { + stmt_if + .elif_else_clauses + .last() + .map_or(true, |clause| needs_next_block(&clause.body)) + }); + + let consequent = + self.append_blocks_if_not_empty(&stmt_if.body, after_consequent_block); + + // Block ID of the next elif or else clause. + let mut next_branch = after_alternate_block; + + for clause in stmt_if.elif_else_clauses.iter().rev() { + let consequent = + self.append_blocks_if_not_empty(&clause.body, after_consequent_block); + + next_branch = if let Some(test) = &clause.test { + let next = NextBlock::If { + condition: Condition::Test(test), + next: consequent, + orelse: next_branch, + }; + let stmts = std::slice::from_ref(stmt); + let block = BasicBlock { stmts, next }; + self.blocks.push(block) + } else { + consequent + }; + } + + NextBlock::If { + condition: Condition::Test(&stmt_if.test), + next: consequent, + orelse: next_branch, + } + } + Stmt::While(StmtWhile { + test: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Test(condition), body, orelse, after), + Stmt::For(StmtFor { + iter: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Iterator(condition), body, orelse, after), + Stmt::Try(StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) => { + // TODO: handle `try` statements. The `try` control flow is very + // complex, what blocks are and aren't taken and from which + // block the control flow is actually returns is **very** + // specific to the contents of the block. Read + // + // very carefully. + // For now we'll skip over it. + let _ = (body, handlers, orelse, finalbody); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::With(StmtWith { items, body, .. }) => { + // TODO: handle `with` statements, see + // . + // I recommend to `try` statements first as `with` can desugar + // to a `try` statement. + // For now we'll skip over it. + let _ = (items, body); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::Match(StmtMatch { subject, cases, .. }) => { + let next_after_block = self.maybe_next_block_index(after, || { + // We don't need need a next block if all cases don't need a + // next block, i.e. if no cases need a next block, and we + // have a wildcard case (to ensure one of the block is + // always taken). + // NOTE: match statement require at least one case, so we + // don't have to worry about empty `cases`. + // TODO: support exhaustive cases without a wildcard. + cases.iter().any(|case| needs_next_block(&case.body)) + || !cases.iter().any(is_wildcard) + }); + let mut orelse_after_block = next_after_block; + for case in cases.iter().rev() { + let block = match_case( + self, + stmt, + subject, + case, + next_after_block, + orelse_after_block, + ); + // For the case above this use the just added case as the + // `orelse` branch, this convert the match statement to + // (essentially) a bunch of if statements. + orelse_after_block = self.blocks.push(block); + } + // TODO: currently we don't include the lines before the match + // statement in the block, unlike what we do for other + // statements. + after = Some(orelse_after_block); + continue; + } + Stmt::Raise(_) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution, it's + // possible it's continued in an `catch` or `finally` block, + // possibly outside of the function. + // Also see `Stmt::Assert` handling. + NextBlock::Terminate + } + Stmt::Assert(stmt) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution if the + // assertion fails, it's possible it's continued in an `catch` + // or `finally` block, possibly outside of the function. + // Also see `Stmt::Raise` handling. + let next = self.force_next_block_index(); + let orelse = self.fake_exception_block_index(); + NextBlock::If { + condition: Condition::Test(&stmt.test), + next, + orelse, + } + } + Stmt::Expr(stmt) => { + match &*stmt.value { + Expr::BoolOp(_) + | Expr::BinOp(_) + | Expr::UnaryOp(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::Compare(_) + | Expr::Call(_) + | Expr::FString(_) + | Expr::StringLiteral(_) + | Expr::BytesLiteral(_) + | Expr::NumberLiteral(_) + | Expr::BooleanLiteral(_) + | Expr::NoneLiteral(_) + | Expr::EllipsisLiteral(_) + | Expr::Attribute(_) + | Expr::Subscript(_) + | Expr::Starred(_) + | Expr::Name(_) + | Expr::List(_) + | Expr::IpyEscapeCommand(_) + | Expr::Tuple(_) + | Expr::Slice(_) => self.unconditional_next_block(after), + // TODO: handle these expressions. + Expr::NamedExpr(_) + | Expr::Lambda(_) + | Expr::IfExp(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::GeneratorExp(_) + | Expr::Await(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) => self.unconditional_next_block(after), + } + } + // The tough branches are done, here is an easy one. + Stmt::Return(_) => NextBlock::Terminate, + }; + + // Include any statements in the block that don't divert the control flow. + let mut start = i; + let end = i + 1; + while stmts_iter + .next_if(|(_, stmt)| !is_control_flow_stmt(stmt)) + .is_some() + { + start -= 1; + } + + let block = BasicBlock { + stmts: &stmts[start..end], + next, + }; + after = Some(self.blocks.push(block)); + } + + after + } + + /// Calls [`create_blocks`] and returns this first block reached (i.e. the last + /// block). + fn append_blocks(&mut self, stmts: &'stmt [Stmt], after: Option) -> BlockIndex { + assert!(!stmts.is_empty()); + self.create_blocks(stmts, after) + .expect("Expect `create_blocks` to create a block if `stmts` is not empty") + } + + /// If `stmts` is not empty this calls [`create_blocks`] and returns this first + /// block reached (i.e. the last block). If `stmts` is empty this returns + /// `after` and doesn't change `blocks`. + fn append_blocks_if_not_empty( + &mut self, + stmts: &'stmt [Stmt], + after: BlockIndex, + ) -> BlockIndex { + if stmts.is_empty() { + after // Empty body, continue with block `after` it. + } else { + self.append_blocks(stmts, Some(after)) + } + } + + /// Select the next block from `blocks` unconditionally. + fn unconditional_next_block(&self, after: Option) -> NextBlock<'static> { + if let Some(after) = after { + return NextBlock::Always(after); + } + + // Either we continue with the next block (that is the last block `blocks`). + // Or it's the last statement, thus we terminate. + self.blocks + .last_index() + .map_or(NextBlock::Terminate, NextBlock::Always) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block. + fn force_next_block_index(&mut self) -> BlockIndex { + self.maybe_next_block_index(None, || true) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block if `condition` returns true. If `condition` returns + /// false the returned index may not be used. + fn maybe_next_block_index( + &mut self, + after: Option, + condition: impl FnOnce() -> bool, + ) -> BlockIndex { + if let Some(after) = after { + // Next block is already determined. + after + } else if let Some(idx) = self.blocks.last_index() { + // Otherwise we either continue with the next block (that is the last + // block in `blocks`). + idx + } else if condition() { + // Or if there are no blocks, but need one based on `condition` than we + // add a fake end block. + self.blocks.push(BasicBlock::EMPTY) + } else { + // NOTE: invalid, but because `condition` returned false this shouldn't + // be used. This only used as an optimisation to avoid adding fake end + // blocks. + BlockIndex::MAX + } + } + + /// Returns a block index for a fake exception block in `blocks`. + fn fake_exception_block_index(&mut self) -> BlockIndex { + for (i, block) in self.blocks.iter_enumerated() { + if block.is_exception() { + return i; + } + } + self.blocks.push(BasicBlock::EXCEPTION) + } + + /// Change the next basic block for the block, or chain of blocks, in index + /// `fixup_index` from `from` to `to`. + /// + /// This doesn't change the target if it's `NextBlock::Terminate`. + fn change_next_block( + &mut self, + mut fixup_index: BlockIndex, + from: BlockIndex, + to: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool + Copy, + ) { + /// Check if we found our target and if `check_condition` is met. + fn is_target( + block: &BasicBlock<'_>, + got: BlockIndex, + expected: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool, + ) -> bool { + got == expected && check_condition(block) + } + + loop { + match self.blocks.get(fixup_index).map(|b| &b.next) { + Some(NextBlock::Always(next)) => { + let next = *next; + if is_target(&self.blocks[fixup_index], next, from, check_condition) { + // Found our target, change it. + self.blocks[fixup_index].next = NextBlock::Always(to); + } + return; + } + Some(NextBlock::If { + condition, + next, + orelse, + }) => { + let idx = fixup_index; + let condition = condition.clone(); + let next = *next; + let orelse = *orelse; + let new_next = if is_target(&self.blocks[idx], next, from, check_condition) { + // Found our target in the next branch, change it (below). + Some(to) + } else { + // Follow the chain. + fixup_index = next; + None + }; + + let new_orelse = if is_target(&self.blocks[idx], orelse, from, check_condition) + { + // Found our target in the else branch, change it (below). + Some(to) + } else if new_next.is_none() { + // If we done with the next branch we only continue with the + // else branch. + fixup_index = orelse; + None + } else { + // If we're not done with the next and else branches we need + // to deal with the else branch before deal with the next + // branch (in the next iteration). + self.change_next_block(orelse, from, to, check_condition); + None + }; + + let (next, orelse) = match (new_next, new_orelse) { + (Some(new_next), Some(new_orelse)) => (new_next, new_orelse), + (Some(new_next), None) => (new_next, orelse), + (None, Some(new_orelse)) => (next, new_orelse), + (None, None) => continue, // Not changing anything. + }; + + self.blocks[idx].next = NextBlock::If { + condition, + next, + orelse, + }; + } + Some(NextBlock::Terminate) | None => return, + } + } + } + + fn finish(mut self) -> BasicBlocks<'stmt> { + if self.blocks.is_empty() { + self.blocks.push(BasicBlock::EMPTY); + } + + BasicBlocks { + blocks: self.blocks, + } + } +} + +impl<'stmt> std::ops::Deref for BasicBlocksBuilder<'stmt> { + type Target = IndexSlice>; + + fn deref(&self) -> &Self::Target { + &self.blocks + } +} + +impl<'stmt> std::ops::DerefMut for BasicBlocksBuilder<'stmt> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.blocks + } +} + +/// Returns true if `stmts` need a next block, false otherwise. +fn needs_next_block(stmts: &[Stmt]) -> bool { + // No statements, we automatically continue with the next block. + let Some(last) = stmts.last() else { + return true; + }; + + match last { + Stmt::Return(_) | Stmt::Raise(_) => false, + Stmt::If(stmt) => needs_next_block(&stmt.body) || stmt.elif_else_clauses.last().map_or(true, |clause| needs_next_block(&clause.body)), + Stmt::FunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::Pass(_) + | Stmt::TypeAlias(_) + | Stmt::IpyEscapeCommand(_) + // TODO: check below. + | Stmt::Break(_) + | Stmt::Continue(_) + | Stmt::For(_) + | Stmt::While(_) + | Stmt::With(_) + | Stmt::Match(_) + | Stmt::Try(_) + | Stmt::Assert(_) => true, + } +} + +/// Returns true if `stmt` contains a control flow statement, e.g. an `if` or +/// `return` statement. +fn is_control_flow_stmt(stmt: &Stmt) -> bool { + match stmt { + Stmt::FunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::TypeAlias(_) + | Stmt::IpyEscapeCommand(_) + | Stmt::Pass(_) => false, + Stmt::Return(_) + | Stmt::For(_) + | Stmt::While(_) + | Stmt::If(_) + | Stmt::With(_) + | Stmt::Match(_) + | Stmt::Raise(_) + | Stmt::Try(_) + | Stmt::Assert(_) + | Stmt::Break(_) + | Stmt::Continue(_) => true, + } +} + +/// Type to create a Mermaid graph. +/// +/// To learn amount Mermaid see , for the syntax +/// see . +struct MermaidGraph<'stmt, 'source> { + graph: &'stmt BasicBlocks<'stmt>, + source: &'source str, +} + +impl<'stmt, 'source> fmt::Display for MermaidGraph<'stmt, 'source> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Flowchart type of graph, top down. + writeln!(f, "flowchart TD")?; + + // List all blocks. + writeln!(f, " start((\"Start\"))")?; + writeln!(f, " return((\"End\"))")?; + for (i, block) in self.graph.blocks.iter().enumerate() { + let (open, close) = if block.is_sentinel() { + ("[[", "]]") + } else { + ("[", "]") + }; + write!(f, " block{i}{open}\"")?; + if block.is_empty() { + write!(f, "`*(empty)*`")?; + } else if block.is_exception() { + write!(f, "Exception raised")?; + } else { + for stmt in block.stmts { + let code_line = &self.source[stmt.range()].trim(); + mermaid_write_quoted_str(f, code_line)?; + write!(f, "\\n")?; + } + } + writeln!(f, "\"{close}")?; + } + writeln!(f)?; + + // Then link all the blocks. + writeln!(f, " start --> block{}", self.graph.blocks.len() - 1)?; + for (i, block) in self.graph.blocks.iter_enumerated().rev() { + let i = i.as_u32(); + match &block.next { + NextBlock::Always(target) => { + writeln!(f, " block{i} --> block{target}", target = target.as_u32())?; + } + NextBlock::If { + condition, + next, + orelse, + } => { + let condition_code = &self.source[condition.range()].trim(); + writeln!( + f, + " block{i} -- \"{condition_code}\" --> block{next}", + next = next.as_u32() + )?; + writeln!( + f, + " block{i} -- \"else\" --> block{orelse}", + orelse = orelse.as_u32() + )?; + } + NextBlock::Terminate => writeln!(f, " block{i} --> return")?, + } + } + + Ok(()) + } +} + +/// Escape double quotes (`"`) in `value` using `#quot;`. +fn mermaid_write_quoted_str(f: &mut fmt::Formatter<'_>, value: &str) -> fmt::Result { + let mut parts = value.split('"'); + if let Some(v) = parts.next() { + write!(f, "{v}")?; + } + for v in parts { + write!(f, "#quot;{v}")?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use ruff_python_parser::{parse, Mode}; + use ruff_text_size::Ranged; + use std::fmt::Write; + use test_case::test_case; + + use crate::rules::ruff::rules::unreachable::{ + BasicBlocks, BlockIndex, MermaidGraph, NextBlock, + }; + + #[test_case("simple.py")] + #[test_case("if.py")] + #[test_case("while.py")] + #[test_case("for.py")] + #[test_case("async-for.py")] + //#[test_case("try.py")] // TODO. + #[test_case("raise.py")] + #[test_case("assert.py")] + #[test_case("match.py")] + fn control_flow_graph(filename: &str) { + let path = PathBuf::from_iter(["resources/test/fixtures/control-flow-graph", filename]); + let source = fs::read_to_string(path).expect("failed to read file"); + let stmts = parse(&source, Mode::Module) + .unwrap_or_else(|err| panic!("failed to parse source: '{source}': {err}")) + .expect_module() + .body; + + let mut output = String::new(); + + for (i, stmts) in stmts.into_iter().enumerate() { + let Some(func) = stmts.function_def_stmt() else { + use std::io::Write; + let _ = std::io::stderr().write_all(b"unexpected statement kind, ignoring"); + continue; + }; + + let got = BasicBlocks::from(&*func.body); + // Basic sanity checks. + assert!(!got.blocks.is_empty(), "basic blocks should never be empty"); + assert_eq!( + got.blocks.first().unwrap().next, + NextBlock::Terminate, + "first block should always terminate" + ); + + let got_mermaid = MermaidGraph { + graph: &got, + source: &source, + }; + + // All block index should be valid. + let valid = BlockIndex::from_usize(got.blocks.len()); + for block in &got.blocks { + match block.next { + NextBlock::Always(index) => assert!(index < valid, "invalid block index"), + NextBlock::If { next, orelse, .. } => { + assert!(next < valid, "invalid next block index"); + assert!(orelse < valid, "invalid orelse block index"); + } + NextBlock::Terminate => {} + } + } + + writeln!( + output, + "## Function {i}\n### Source\n```python\n{}\n```\n\n### Control Flow Graph\n```mermaid\n{}```\n", + &source[func.range()], + got_mermaid + ) + .unwrap(); + } + + insta::with_settings!({ + omit_expression => true, + input_file => filename, + description => "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." + }, { + insta::assert_snapshot!(format!("{filename}.md"), output); + }); + } +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF014_RUF014.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF014_RUF014.py.snap new file mode 100644 index 00000000000000..a30be8cd6e71bd --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF014_RUF014.py.snap @@ -0,0 +1,249 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF014.py:3:5: RUF014 Unreachable code in after_return + | +1 | def after_return(): +2 | return "reachable" +3 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +4 | +5 | async def also_works_on_async_functions(): + | + +RUF014.py:7:5: RUF014 Unreachable code in also_works_on_async_functions + | +5 | async def also_works_on_async_functions(): +6 | return "reachable" +7 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +8 | +9 | def if_always_true(): + | + +RUF014.py:12:5: RUF014 Unreachable code in if_always_true + | +10 | if True: +11 | return "reachable" +12 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +13 | +14 | def if_always_false(): + | + +RUF014.py:16:9: RUF014 Unreachable code in if_always_false + | +14 | def if_always_false(): +15 | if False: +16 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +17 | return "reachable" + | + +RUF014.py:21:9: RUF014 Unreachable code in if_elif_always_false + | +19 | def if_elif_always_false(): +20 | if False: +21 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +22 | elif False: +23 | return "also unreachable" + | + +RUF014.py:23:9: RUF014 Unreachable code in if_elif_always_false + | +21 | return "unreachable" +22 | elif False: +23 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +24 | return "reachable" + | + +RUF014.py:28:9: RUF014 Unreachable code in if_elif_always_true + | +26 | def if_elif_always_true(): +27 | if False: +28 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +29 | elif True: +30 | return "reachable" + | + +RUF014.py:31:5: RUF014 Unreachable code in if_elif_always_true + | +29 | elif True: +30 | return "reachable" +31 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +32 | +33 | def ends_with_if(): + | + +RUF014.py:35:9: RUF014 Unreachable code in ends_with_if + | +33 | def ends_with_if(): +34 | if False: +35 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +36 | else: +37 | return "reachable" + | + +RUF014.py:42:5: RUF014 Unreachable code in infinite_loop + | +40 | while True: +41 | continue +42 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +43 | +44 | ''' TODO: we could determine these, but we don't yet. + | + +RUF014.py:75:5: RUF014 Unreachable code in match_wildcard + | +73 | case _: +74 | return "reachable" +75 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +76 | +77 | def match_case_and_wildcard(status): + | + +RUF014.py:83:5: RUF014 Unreachable code in match_case_and_wildcard + | +81 | case _: +82 | return "reachable" +83 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +84 | +85 | def raise_exception(): + | + +RUF014.py:87:5: RUF014 Unreachable code in raise_exception + | +85 | def raise_exception(): +86 | raise Exception +87 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +88 | +89 | def while_false(): + | + +RUF014.py:91:9: RUF014 Unreachable code in while_false + | +89 | def while_false(): +90 | while False: +91 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +92 | return "reachable" + | + +RUF014.py:96:9: RUF014 Unreachable code in while_false_else + | +94 | def while_false_else(): +95 | while False: +96 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +97 | else: +98 | return "reachable" + | + +RUF014.py:102:9: RUF014 Unreachable code in while_false_else_return + | +100 | def while_false_else_return(): +101 | while False: +102 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +103 | else: +104 | return "reachable" + | + +RUF014.py:105:5: RUF014 Unreachable code in while_false_else_return + | +103 | else: +104 | return "reachable" +105 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +106 | +107 | def while_true(): + | + +RUF014.py:110:5: RUF014 Unreachable code in while_true + | +108 | while True: +109 | return "reachable" +110 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +111 | +112 | def while_true_else(): + | + +RUF014.py:116:9: RUF014 Unreachable code in while_true_else + | +114 | return "reachable" +115 | else: +116 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +117 | +118 | def while_true_else_return(): + | + +RUF014.py:122:9: RUF014 Unreachable code in while_true_else_return + | +120 | return "reachable" +121 | else: +122 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +123 | return "also unreachable" + | + +RUF014.py:123:5: RUF014 Unreachable code in while_true_else_return + | +121 | else: +122 | return "unreachable" +123 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +124 | +125 | def while_false_var_i(): + | + +RUF014.py:128:9: RUF014 Unreachable code in while_false_var_i + | +126 | i = 0 +127 | while False: +128 | i += 1 + | ^^^^^^ RUF014 +129 | return i + | + +RUF014.py:135:5: RUF014 Unreachable code in while_true_var_i + | +133 | while True: +134 | i += 1 +135 | return i + | ^^^^^^^^ RUF014 +136 | +137 | def while_infinite(): + | + +RUF014.py:140:5: RUF014 Unreachable code in while_infinite + | +138 | while True: +139 | pass +140 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +141 | +142 | def while_if_true(): + | + +RUF014.py:146:5: RUF014 Unreachable code in while_if_true + | +144 | if True: +145 | return "reachable" +146 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +147 | +148 | # Test case found in the Bokeh repository that trigger a false positive. + | + +