-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement ForVariableUsedAfterBlock
(based on wemake_python_styleguide
ControlVarUsedAfterBlockViolation
WPS441
)
#11769
Open
MadLittleMods
wants to merge
38
commits into
astral-sh:main
Choose a base branch
from
MadLittleMods:madlittlemods/control-var-used-after-block
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 15 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
c1091a1
Rough start
MadLittleMods 261fbc3
Rough run-able
MadLittleMods 8bed302
Lookup by reference
MadLittleMods e85fe87
Add todo
MadLittleMods eecc2af
Checkpoint working with Olivier
MadLittleMods 3661066
Work in all scopes
MadLittleMods 59a2e3b
Clean up code
MadLittleMods 0e491b3
Adjust some wording
MadLittleMods 9ca72a6
Move to wemake_python_styleguide ruleset
MadLittleMods 0ad0df0
Commit test snapshot
MadLittleMods eaf2a35
Fix shadowed cases
MadLittleMods 710d68c
Clean up debug
MadLittleMods a7de1cb
Fix references before
MadLittleMods f2a62bb
Adjust wording
MadLittleMods 97b165c
Update snapshot
MadLittleMods 7ceb306
Move to Ruff (RUF) ruleset
MadLittleMods acc5b2a
Merge branch 'main' into madlittlemods/control-var-used-after-block
MadLittleMods f5aa633
Merge branch 'main' into madlittlemods/control-var-used-after-block
MadLittleMods 026a48f
Use preview group
MadLittleMods 544bbf5
Separate out `in` expression
MadLittleMods 2d13a62
Better comment to explain why source-order
MadLittleMods 1d18c66
No need to `collect()`
MadLittleMods 7edd539
Rename to `ForVariableUsedAfterBlock`
MadLittleMods 5351952
Remove whitespace change
MadLittleMods 19a0ab9
Nit: Use labeled loops
MadLittleMods 9e2ece4
No need for `BlockKind` anymore
MadLittleMods d675a03
Use `statement_id(...)` inside `statement(...)`
MadLittleMods 71ca5e1
Fix lint warnings about unused imports
MadLittleMods 8a11073
Add nested function/class example
MadLittleMods e59efa2
Remove `known_good_reference_node_ids` and iterating bindings in reverse
MadLittleMods 5a76166
Get `binding_statement` in smarter way that avoids `unwrap()`
MadLittleMods 106e78a
Better document why when `binding.statement(...)` can be `None`
MadLittleMods 8b3e4de
Test augmented assignment
MadLittleMods 59427f1
Reword
MadLittleMods 63ecbfc
Explain more `unwrap()`
MadLittleMods 27dad4f
Update snapshot
MadLittleMods e7653df
Merge branch 'main' into madlittlemods/control-var-used-after-block
MadLittleMods 9bcc4fe
Fix snapshot after bumping rule number
MadLittleMods File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
...f_linter/resources/test/fixtures/wemake_python_styleguide/control_var_used_after_block.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
for global_var in []: | ||
MichaReiser marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_ = global_var | ||
pass | ||
|
||
# ❌ | ||
_ = global_var | ||
|
||
def foo(): | ||
# For control var used outside block | ||
for event in []: | ||
_ = event | ||
pass | ||
|
||
# ❌ | ||
_ = event | ||
|
||
for x in []: | ||
_ = x | ||
pass | ||
|
||
# Using the same control variable name in a different loop is ok | ||
for x in []: | ||
_ = x | ||
pass | ||
|
||
for y in []: | ||
# ❌ x is used outside of the loop it was defined in (meant to use y) | ||
if x == 5: | ||
pass | ||
|
||
# Assign a variable before the loop | ||
room_id = 3 | ||
_ = room_id | ||
# Use the same variable name in a loop | ||
for room_id in []: | ||
_ = room_id | ||
pass | ||
|
||
# ❌ After the loop is not ok because the value is probably not what you expect | ||
_ = room_id | ||
|
||
# Tuple destructuring | ||
for a, b, c in []: | ||
_ = a | ||
_ = b | ||
_ = c | ||
pass | ||
|
||
# ❌ | ||
_ = a | ||
_ = b | ||
_ = c | ||
|
||
# Array destructuring | ||
for [d, e, f] in []: | ||
_ = d | ||
_ = e | ||
_ = f | ||
pass | ||
|
||
# ❌ | ||
_ = d | ||
_ = e | ||
_ = f | ||
|
||
# with statement | ||
with None as w: | ||
_ = w | ||
pass | ||
|
||
# ❌ | ||
_ = w | ||
|
||
# Nested blocks | ||
with None as q: | ||
_ = q | ||
|
||
for n in []: | ||
_ = n | ||
_ = q | ||
pass | ||
|
||
# ❌ | ||
_ = n | ||
|
||
# ❌ | ||
_ = q | ||
_ = n | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,3 +57,4 @@ pub mod pyupgrade; | |
pub mod refurb; | ||
pub mod ruff; | ||
pub mod tryceratops; | ||
pub mod wemake_python_styleguide; |
27 changes: 27 additions & 0 deletions
27
crates/ruff_linter/src/rules/wemake_python_styleguide/mod.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
pub(crate) mod rules; | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::path::Path; | ||
|
||
use anyhow::Result; | ||
use test_case::test_case; | ||
|
||
use crate::registry::Rule; | ||
use crate::test::test_path; | ||
use crate::{assert_messages, settings}; | ||
|
||
#[test_case( | ||
Rule::ControlVarUsedAfterBlock, | ||
Path::new("control_var_used_after_block.py") | ||
)] | ||
fn rules(rule_code: Rule, path: &Path) -> Result<()> { | ||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); | ||
let diagnostics = test_path( | ||
Path::new("wemake_python_styleguide").join(path).as_path(), | ||
&settings::LinterSettings::for_rule(rule_code), | ||
)?; | ||
assert_messages!(snapshot, diagnostics); | ||
Ok(()) | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
crates/ruff_linter/src/rules/wemake_python_styleguide/rules/control_var_used_after_block.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
use std::fmt; | ||
|
||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::Stmt; | ||
use ruff_python_semantic::{BindingId, NodeId, Scope}; | ||
use ruff_text_size::Ranged; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for variables defined in `for` loops and `with` statements that are used | ||
/// outside of their respective blocks. | ||
/// | ||
/// ## Why is this bad? | ||
/// Usage of of a control variable outside of the block they're defined in will probably | ||
/// lead to flawed logic in a way that will likely cause bugs. The variable might not | ||
/// contain what you expect. | ||
/// | ||
/// In Python, unlike many other languages, `for` loops and `with` statements don't | ||
/// define their own scopes. Therefore, usage of the control variables outside of the | ||
/// block will be the the value from the last iteration until re-assigned. | ||
/// | ||
/// While this mistake is easy to spot in small examples, it can be hidden in larger | ||
/// blocks of code, where the the loop and downstream usage may not be visible at the | ||
/// same time. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// for x in range(10): | ||
/// pass | ||
/// | ||
/// print(x) # prints 9 | ||
/// | ||
/// with path.open() as f: | ||
/// pass | ||
/// | ||
/// print(f.readline()) # prints a line from a file that is already closed (error) | ||
/// ``` | ||
#[violation] | ||
pub struct ControlVarUsedAfterBlock { | ||
control_var_name: String, | ||
block_kind: BlockKind, | ||
} | ||
|
||
impl Violation for ControlVarUsedAfterBlock { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
let ControlVarUsedAfterBlock { | ||
control_var_name, | ||
block_kind, | ||
} = self; | ||
|
||
format!("{block_kind} variable {control_var_name} is used outside of block") | ||
} | ||
} | ||
|
||
#[derive(Debug, PartialEq, Eq, Clone, Copy)] | ||
enum BlockKind { | ||
For, | ||
With, | ||
} | ||
|
||
impl fmt::Display for BlockKind { | ||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
match self { | ||
BlockKind::For => fmt.write_str("`for` loop"), | ||
BlockKind::With => fmt.write_str("`with` statement"), | ||
} | ||
} | ||
} | ||
|
||
/// WPS441: Forbid control variables after the block body. | ||
pub(crate) fn control_var_used_after_block( | ||
checker: &Checker, | ||
scope: &Scope, | ||
diagnostics: &mut Vec<Diagnostic>, | ||
) { | ||
// Keep track of node_ids we know are used in the same block. This helps us when | ||
// people use the same variable name in multiple blocks. | ||
let mut known_good_reference_node_ids: Vec<NodeId> = Vec::new(); | ||
|
||
let all_bindings: Vec<(&str, BindingId)> = scope.all_bindings().collect(); | ||
// We want to reverse the bindings so that we iterate in source order and shadowed | ||
// bindings come first. | ||
let reversed_bindings = all_bindings.iter().rev(); | ||
|
||
// Find for-loop and with-statement variable bindings | ||
for (&name, binding) in reversed_bindings | ||
.map(|(name, binding_id)| (name, checker.semantic().binding(*binding_id))) | ||
.filter_map(|(name, binding)| { | ||
if binding.kind.is_loop_var() || binding.kind.is_with_item_var() { | ||
return Some((name, binding)); | ||
} | ||
|
||
None | ||
}) | ||
{ | ||
let binding_statement = binding.statement(checker.semantic()).unwrap(); | ||
let binding_source_node_id = binding.source.unwrap(); | ||
// The node_id of the for-loop that contains the binding | ||
let binding_statement_id = checker.semantic().statement_id(binding_source_node_id); | ||
|
||
// Loop over the references of those bindings to see if they're in the same block-scope | ||
for reference in binding.references() { | ||
let reference = checker.semantic().reference(reference); | ||
let reference_node_id = reference.expression_id().unwrap(); | ||
|
||
// Skip any reference that come before the control var binding in the source | ||
// order, skip it because people can assign and use the same variable name | ||
// above the block. | ||
if reference.range().end() < binding_statement.range().start() { | ||
continue; | ||
} | ||
|
||
// Traverse the hierarchy and look for a block match | ||
let statement_hierarchy: Vec<NodeId> = checker | ||
.semantic() | ||
.parent_statement_ids(reference_node_id) | ||
.collect(); | ||
|
||
let mut is_used_in_block = false; | ||
for ancestor_node_id in statement_hierarchy { | ||
if binding_statement_id == ancestor_node_id { | ||
is_used_in_block = true; | ||
known_good_reference_node_ids.push(reference_node_id); | ||
break; | ||
} | ||
} | ||
|
||
// If the reference wasn't used in the same block, report a violation/diagnostic | ||
if !is_used_in_block && !known_good_reference_node_ids.contains(&reference_node_id) { | ||
let block_kind = match binding_statement { | ||
Stmt::For(_) => BlockKind::For, | ||
Stmt::With(_) => BlockKind::With, | ||
_ => { | ||
panic!("Unexpected block item. This is a problem with ruff itself. Fix the `filter_map` above.") | ||
} | ||
}; | ||
|
||
diagnostics.push(Diagnostic::new( | ||
ControlVarUsedAfterBlock { | ||
control_var_name: name.to_owned(), | ||
block_kind, | ||
}, | ||
reference.range(), | ||
)); | ||
} | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
crates/ruff_linter/src/rules/wemake_python_styleguide/rules/mod.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
pub(crate) use control_var_used_after_block::*; | ||
|
||
mod control_var_used_after_block; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Friendly ping @charliermarsh 🙇
Just looking for the next steps here (whether that be in a queue, pinging another person, etc)