Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/wps_light/WPS362.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
a[1:3] = [1, 2]
a[slice(1)] = [1, 3]

a[:0], a[-1:] = prefix, suffix

a[:] = complete_replacement

a[0:0] = prepend

a[0] = 1


size = 3
board = [["_"] * size for _ in range(size)]

board[0:1][1:2] = "X"

board[0] = board[0:1] = board[1:2] = [board[0][:], board[1][:], board[2][:]] = board
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::rules::{
flake8_pie, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify,
flake8_slots, flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming,
perflint, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
wps_light,
};
use crate::settings::types::PythonVersion;

Expand Down Expand Up @@ -1650,6 +1651,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695TypeAlias) {
pyupgrade::rules::non_pep695_type_alias_type(checker, assign);
}
if checker.enabled(Rule::AssignmentToSubscriptSlice) {
wps_light::rules::assignment_to_subscript_slice(checker, assign);
}
}
Stmt::AnnAssign(
assign_stmt @ ast::StmtAnnAssign {
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
(Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException),

// wps-light
(WpsLight, "362") => (RuleGroup::Preview, rules::wps_light::rules::AssignmentToSubscriptSlice),

// ruff
(Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString),
(Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring),
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ pub enum Linter {
/// [pydoclint](https://pypi.org/project/pydoclint/)
#[prefix = "DOC"]
Pydoclint,
/// [wps-light](https://pypi.org/project/wps-light/)
#[prefix = "WPS"]
WpsLight,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ pub mod pyupgrade;
pub mod refurb;
pub mod ruff;
pub mod tryceratops;
pub mod wps_light;
26 changes: 26 additions & 0 deletions crates/ruff_linter/src/rules/wps_light/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Rules from [wps-light](https://pypi.org/project/wps-light/).
pub(crate) mod rules;

#[cfg(test)]
mod tests {
use std::convert::AsRef;
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::AssignmentToSubscriptSlice, Path::new("WPS362.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("wps_light").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, StmtAssign};
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;

/// ## What it does
/// This rule checks for assignments to a subscript slice.
///
/// ## Why is this bad?
/// Assignment to a slice can implicitly change the size of a list,
/// leading to potential bugs that are difficult to spot.
///
/// ## Example
/// ```python
/// a[1:3] = [1, 2]
/// a[slice(1)] = [1, 3]
/// ```
///
/// Use instead:
/// Instead of using slice assignment, modify individual elements to avoid implicit size changes and potential confusion:
/// ```python
/// a[5] = 1
/// ```
///
/// ## Notes
/// - A common example of this, which violates the rule, is in-place list replacement using `[:]`.
/// This approach can replace the entire content of the list while maintaining the same object reference.
/// - Slice assignment is only in-place replacement of multiple array elements.
///
/// ## References
/// - [Python documentation: Assign](https://docs.python.org/3/library/ast.html#ast.Assign)
#[violation]
pub struct AssignmentToSubscriptSlice;

impl Violation for AssignmentToSubscriptSlice {
#[derive_message_formats]
fn message(&self) -> String {
"Assignment to a subscript slice found".to_string()
}
}

/// WPS362
pub(crate) fn assignment_to_subscript_slice(checker: &mut Checker, stmt: &StmtAssign) {
let StmtAssign { targets, .. } = stmt;
targets
.iter()
.for_each(|target| check_subscript_slice(checker, target));
}

fn check_subscript_slice(checker: &mut Checker, expr: &Expr) {
match expr {
Expr::Subscript(expr_subscript) => {
if !expr_subscript.slice.is_slice_expr() {
return;
}
checker
.diagnostics
.push(Diagnostic::new(AssignmentToSubscriptSlice, expr.range()));
}
Expr::List(expr_list) => expr_list
.elts
.iter()
.for_each(|item| check_subscript_slice(checker, item)),
Expr::Tuple(expr_tuple) => expr_tuple
.elts
.iter()
.for_each(|item| check_subscript_slice(checker, item)),
_ => {}
}
}
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/rules/wps_light/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub(crate) use assignment_to_subscript_slice::*;

mod assignment_to_subscript_slice;
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
source: crates/ruff_linter/src/rules/wps_light/mod.rs
snapshot_kind: text
---
WPS362.py:1:1: WPS362 Assignment to a subscript slice found
|
1 | a[1:3] = [1, 2]
| ^^^^^^ WPS362
2 | a[slice(1)] = [1, 3]
|

WPS362.py:4:1: WPS362 Assignment to a subscript slice found
|
2 | a[slice(1)] = [1, 3]
3 |
4 | a[:0], a[-1:] = prefix, suffix
| ^^^^^ WPS362
5 |
6 | a[:] = complete_replacement
|

WPS362.py:4:8: WPS362 Assignment to a subscript slice found
|
2 | a[slice(1)] = [1, 3]
3 |
4 | a[:0], a[-1:] = prefix, suffix
| ^^^^^^ WPS362
5 |
6 | a[:] = complete_replacement
|

WPS362.py:6:1: WPS362 Assignment to a subscript slice found
|
4 | a[:0], a[-1:] = prefix, suffix
5 |
6 | a[:] = complete_replacement
| ^^^^ WPS362
7 |
8 | a[0:0] = prepend
|

WPS362.py:8:1: WPS362 Assignment to a subscript slice found
|
6 | a[:] = complete_replacement
7 |
8 | a[0:0] = prepend
| ^^^^^^ WPS362
9 |
10 | a[0] = 1
|

WPS362.py:16:1: WPS362 Assignment to a subscript slice found
|
14 | board = [["_"] * size for _ in range(size)]
15 |
16 | board[0:1][1:2] = "X"
| ^^^^^^^^^^^^^^^ WPS362
17 |
18 | board[0] = board[0:1] = board[1:2] = [board[0][:], board[1][:], board[2][:]] = board
|

WPS362.py:18:12: WPS362 Assignment to a subscript slice found
|
16 | board[0:1][1:2] = "X"
17 |
18 | board[0] = board[0:1] = board[1:2] = [board[0][:], board[1][:], board[2][:]] = board
| ^^^^^^^^^^ WPS362
|

WPS362.py:18:25: WPS362 Assignment to a subscript slice found
|
16 | board[0:1][1:2] = "X"
17 |
18 | board[0] = board[0:1] = board[1:2] = [board[0][:], board[1][:], board[2][:]] = board
| ^^^^^^^^^^ WPS362
|

WPS362.py:18:39: WPS362 Assignment to a subscript slice found
|
16 | board[0:1][1:2] = "X"
17 |
18 | board[0] = board[0:1] = board[1:2] = [board[0][:], board[1][:], board[2][:]] = board
| ^^^^^^^^^^^ WPS362
|

WPS362.py:18:52: WPS362 Assignment to a subscript slice found
|
16 | board[0:1][1:2] = "X"
17 |
18 | board[0] = board[0:1] = board[1:2] = [board[0][:], board[1][:], board[2][:]] = board
| ^^^^^^^^^^^ WPS362
|

WPS362.py:18:65: WPS362 Assignment to a subscript slice found
|
16 | board[0:1][1:2] = "X"
17 |
18 | board[0] = board[0:1] = board[1:2] = [board[0][:], board[1][:], board[2][:]] = board
| ^^^^^^^^^^^ WPS362
|
4 changes: 4 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.