mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:24 +00:00
![]() ## Summary We have a few rules that rely on detecting whether two statements are in different branches -- for example, different arms of an `if`-`else`. Historically, the way this was implemented is that, given two statement IDs, we'd find the common parent (by traversing upwards via our `Statements` abstraction); then identify branches "manually" by matching the parents against `try`, `if`, and `match`, and returning iterators over the arms; then check if there's an arm for which one of the statements is a child, and the other is not. This has a few drawbacks: 1. First, the code is generally a bit hard to follow (Konsti mentioned this too when working on the `ElifElseClause` refactor). 2. Second, this is the only place in the codebase where we need to go from `&Stmt` to `StatementID` -- _everywhere_ else, we only need to go in the _other_ direction. Supporting these lookups means we need to maintain a mapping from `&Stmt` to `StatementID` that includes every `&Stmt` in the program. (We _also_ end up maintaining a `depth` level for every statement.) I'd like to get rid of these requirements to improve efficiency, reduce complexity, and enable us to treat AST modes more generically in the future. (When I looked at adding the `&Expr` to our existing statement-tracking infrastructure, maintaining a hash map with all the statements noticeably hurt performance.) The solution implemented here instead makes branches a first-class concept in the semantic model. Like with `Statements`, we now have a `Branches` abstraction, where each branch points to its optional parent. When we store statements, we store the `BranchID` alongside each statement. When we need to detect whether two statements are in the same branch, we just realize each statement's branch path and compare the two. (Assuming that the two statements are in the same scope, then they're on the same branch IFF one branch path is a subset of the other, starting from the top.) We then add some calls to the visitor to push and pop branches in the appropriate places, for `if`, `try`, and `match` statements. Note that a branch is not 1:1 with a statement; instead, each branch is closer to a suite, but not _every_ suite is a branch. For example, each arm in an `if`-`elif`-`else` is a branch, but the `else` in a `for` loop is not considered a branch. In addition to being much simpler, this should also be more efficient, since we've shed the entire `&Stmt` hash map, plus the `depth` that we track on `StatementWithParent` in favor of a single `Option<BranchID>` on `StatementWithParent` plus a single vector for all branches. The lookups should be faster too, since instead of doing a bunch of jumps around with the hash map + repeated recursive calls to find the common parents, we instead just do a few simple lookups in the `Branches` vector to realize and compare the branch paths. ## Test Plan `cargo test` -- we have a lot of coverage for this, which we inherited from PyFlakes |
||
---|---|---|
.. | ||
src | ||
Cargo.toml |