Control flow: return and raise (#17121)

We add support for `return` and `raise` statements in the control flow
graph: we simply add an edge to the terminal block, push the statements
to the current block, and proceed.

This implementation will have to be modified somewhat once we add
support for `try` statements - then we will need to check whether to
_defer_ the jump. But for now this will do!

Also in this PR: We fix the `unreachable` diagnostic range so that it
lumps together consecutive unreachable blocks.
This commit is contained in:
Dylan 2025-04-03 08:30:29 -05:00 committed by GitHub
parent 755ece0c36
commit d401a5440e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 194 additions and 14 deletions

View file

@ -12,3 +12,19 @@ def no_control_flow_reachable():
del c
def foo():
return
def after_return():
return 1
print("unreachable")
print("and this")
def after_raise():
raise ValueError
print("unreachable")
print("and this")
def multiple_returns():
return 1
print("unreachable")
return 2
print("unreachable range should include above return")

View file

@ -3,7 +3,7 @@ use std::collections::HashSet;
use itertools::Itertools;
use ruff_python_ast::{Identifier, Stmt};
use ruff_python_semantic::cfg::graph::{build_cfg, BlockId, Condition, ControlFlowGraph};
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
@ -46,19 +46,24 @@ pub(crate) fn in_function(checker: &Checker, name: &Identifier, body: &[Stmt]) {
let cfg = build_cfg(body);
let reachable = reachable(&cfg);
let mut unreachable = (0..cfg.num_blocks())
let mut blocks = (0..cfg.num_blocks())
.map(BlockId::from_usize)
.filter(|block| !reachable.contains(block) && !cfg.stmts(*block).is_empty())
.map(|block| cfg.range(block))
.sorted_by_key(ruff_text_size::Ranged::start)
.filter(|block| !cfg.stmts(*block).is_empty())
.sorted_by_key(|block| cfg.range(*block).start())
.peekable();
while let Some(block_range) = unreachable.next() {
let start = block_range.start();
let mut end = block_range.end();
while let Some(next_block) = unreachable.next_if(|nxt| nxt.start() <= end) {
end = next_block.end();
// Advance past leading reachable blocks
while blocks.next_if(|block| reachable.contains(block)).is_some() {}
while let Some(start_block) = blocks.next() {
// Advance to next reachable block
let mut end_block = start_block;
while let Some(next_block) = blocks.next_if(|block| !reachable.contains(block)) {
end_block = next_block;
}
let start = cfg.range(start_block).start();
let end = cfg.range(end_block).end();
checker.report_diagnostic(Diagnostic::new(
UnreachableCode {
name: name.to_string(),

View file

@ -1,4 +1,34 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unreachable.py:18:5: PLW0101 Unreachable code in `after_return`
|
16 | def after_return():
17 | return 1
18 | / print("unreachable")
19 | | print("and this")
| |_____________________^ PLW0101
20 |
21 | def after_raise():
|
unreachable.py:23:5: PLW0101 Unreachable code in `after_raise`
|
21 | def after_raise():
22 | raise ValueError
23 | / print("unreachable")
24 | | print("and this")
| |_____________________^ PLW0101
25 |
26 | def multiple_returns():
|
unreachable.py:28:5: PLW0101 Unreachable code in `multiple_returns`
|
26 | def multiple_returns():
27 | return 1
28 | / print("unreachable")
29 | | return 2
30 | | print("unreachable range should include above return")
| |__________________________________________________________^ PLW0101
|