diff --git a/README.md b/README.md index 2399c9015e..e761149f62 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ ruff's goal is to achieve feature-parity with Flake8 when used (1) without any p stylistic checks; limiting to Python 3 obviates the need for certain compatibility checks.) Under those conditions, Flake8 implements about 58 rules, give or take. At time of writing, ruff -implements 24 rules. (Note that these 24 rules likely cover a disproportionate share of errors: +implements 28 rules. (Note that these 28 rules likely cover a disproportionate share of errors: unused imports, undefined variables, etc.) Of the unimplemented rules, ruff is missing: @@ -158,6 +158,8 @@ Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis F | F541 | FStringMissingPlaceholders | f-string without any placeholders | | F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | | F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | +| F621 | TooManyExpressionsInStarredAssignment | too many expressions in star-unpacking assignment | +| F622 | TwoStarredExpressions | two starred expressions in assignment | | F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | | F634 | IfTuple | If test is a tuple, which is always `True` | | F704 | YieldOutsideFunction | a `yield` or `yield from` statement outside of a function/method | diff --git a/examples/generate_rules_table.rs b/examples/generate_rules_table.rs index 19422a8789..ac72924765 100644 --- a/examples/generate_rules_table.rs +++ b/examples/generate_rules_table.rs @@ -21,7 +21,9 @@ fn main() { CheckKind::NotIsTest, CheckKind::RaiseNotImplemented, CheckKind::ReturnOutsideFunction, + CheckKind::TooManyExpressionsInStarredAssignment, CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq), + CheckKind::TwoStarredExpressions, CheckKind::UndefinedExport("...".to_string()), CheckKind::UndefinedLocal("...".to_string()), CheckKind::UndefinedName("...".to_string()), diff --git a/resources/test/fixtures/F622.py b/resources/test/fixtures/F622.py new file mode 100644 index 0000000000..23f6a31ba8 --- /dev/null +++ b/resources/test/fixtures/F622.py @@ -0,0 +1,3 @@ +*a, *b, c = (1, 2, 3) +*a, b, c = (1, 2, 3) +a, b, *c = (1, 2, 3) diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index c5f97c784d..6109a1843d 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -15,6 +15,8 @@ select = [ "F541", "F601", "F602", + "F621", + "F622", "F631", "F634", "F704", diff --git a/src/ast/checks.rs b/src/ast/checks.rs index cdf6094168..8df3edd794 100644 --- a/src/ast/checks.rs +++ b/src/ast/checks.rs @@ -395,3 +395,36 @@ pub fn check_literal_comparisons( checks } + +/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance. +pub fn check_starred_expressions( + elts: &[Expr], + location: Location, + check_too_many_expressions: bool, + check_two_starred_expressions: bool, +) -> Option { + let mut has_starred: bool = false; + let mut starred_index: Option = None; + for (index, elt) in elts.iter().enumerate() { + if matches!(elt.node, ExprKind::Starred { .. }) { + if has_starred && check_two_starred_expressions { + return Some(Check::new(CheckKind::TwoStarredExpressions, location)); + } + has_starred = true; + starred_index = Some(index); + } + } + + if check_too_many_expressions { + if let Some(starred_index) = starred_index { + if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 { + return Some(Check::new( + CheckKind::TooManyExpressionsInStarredAssignment, + location, + )); + } + } + } + + None +} diff --git a/src/check_ast.rs b/src/check_ast.rs index 1c377f1a5b..95d3350a99 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -430,6 +430,22 @@ where self.in_literal = true; } } + ExprKind::Tuple { elts, ctx } => { + if matches!(ctx, ExprContext::Store) { + let check_too_many_expressions = + self.settings.select.contains(&CheckCode::F621); + let check_two_starred_expressions = + self.settings.select.contains(&CheckCode::F622); + if let Some(check) = checks::check_starred_expressions( + elts, + expr.location, + check_too_many_expressions, + check_two_starred_expressions, + ) { + self.checks.push(check); + } + } + } ExprKind::Name { ctx, .. } => match ctx { ExprContext::Load => self.handle_node_load(expr), ExprContext::Store => { diff --git a/src/checks.rs b/src/checks.rs index 85d9c1ca01..0b7e390184 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -21,6 +21,8 @@ pub enum CheckCode { F541, F601, F602, + F621, + F622, F631, F634, F704, @@ -54,6 +56,8 @@ impl FromStr for CheckCode { "F541" => Ok(CheckCode::F541), "F601" => Ok(CheckCode::F601), "F602" => Ok(CheckCode::F602), + "F621" => Ok(CheckCode::F621), + "F622" => Ok(CheckCode::F622), "F631" => Ok(CheckCode::F631), "F634" => Ok(CheckCode::F634), "F704" => Ok(CheckCode::F704), @@ -88,6 +92,8 @@ impl CheckCode { CheckCode::F541 => "F541", CheckCode::F601 => "F601", CheckCode::F602 => "F602", + CheckCode::F621 => "F621", + CheckCode::F622 => "F622", CheckCode::F631 => "F631", CheckCode::F634 => "F634", CheckCode::F704 => "F704", @@ -120,6 +126,8 @@ impl CheckCode { CheckCode::F541 => &LintSource::AST, CheckCode::F601 => &LintSource::AST, CheckCode::F602 => &LintSource::AST, + CheckCode::F621 => &LintSource::AST, + CheckCode::F622 => &LintSource::AST, CheckCode::F631 => &LintSource::AST, CheckCode::F634 => &LintSource::AST, CheckCode::F704 => &LintSource::AST, @@ -170,7 +178,9 @@ pub enum CheckKind { NotIsTest, RaiseNotImplemented, ReturnOutsideFunction, + TooManyExpressionsInStarredAssignment, TrueFalseComparison(bool, RejectedCmpop), + TwoStarredExpressions, UndefinedExport(String), UndefinedLocal(String), UndefinedName(String), @@ -202,7 +212,11 @@ impl CheckKind { CheckKind::NotIsTest => "NotIsTest", CheckKind::RaiseNotImplemented => "RaiseNotImplemented", CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction", + CheckKind::TooManyExpressionsInStarredAssignment => { + "TooManyExpressionsInStarredAssignment" + } CheckKind::TrueFalseComparison(_, _) => "TrueFalseComparison", + CheckKind::TwoStarredExpressions => "TwoStarredExpressions", CheckKind::UndefinedExport(_) => "UndefinedExport", CheckKind::UndefinedLocal(_) => "UndefinedLocal", CheckKind::UndefinedName(_) => "UndefinedName", @@ -234,7 +248,9 @@ impl CheckKind { CheckKind::NotIsTest => &CheckCode::E714, CheckKind::RaiseNotImplemented => &CheckCode::F901, CheckKind::ReturnOutsideFunction => &CheckCode::F706, + CheckKind::TooManyExpressionsInStarredAssignment => &CheckCode::F621, CheckKind::TrueFalseComparison(_, _) => &CheckCode::E712, + CheckKind::TwoStarredExpressions => &CheckCode::F622, CheckKind::UndefinedExport(_) => &CheckCode::F822, CheckKind::UndefinedLocal(_) => &CheckCode::F823, CheckKind::UndefinedName(_) => &CheckCode::F821, @@ -295,6 +311,9 @@ impl CheckKind { CheckKind::ReturnOutsideFunction => { "a `return` statement outside of a function/method".to_string() } + CheckKind::TooManyExpressionsInStarredAssignment => { + "too many expressions in star-unpacking assignment".to_string() + } CheckKind::TrueFalseComparison(value, op) => match *value { true => match op { RejectedCmpop::Eq => { @@ -313,6 +332,7 @@ impl CheckKind { } }, }, + CheckKind::TwoStarredExpressions => "two starred expressions in assignment".to_string(), CheckKind::UndefinedExport(name) => { format!("Undefined name `{name}` in `__all__`") } @@ -356,7 +376,9 @@ impl CheckKind { CheckKind::NoneComparison(_) => false, CheckKind::RaiseNotImplemented => false, CheckKind::ReturnOutsideFunction => false, + CheckKind::TooManyExpressionsInStarredAssignment => false, CheckKind::TrueFalseComparison(_, _) => false, + CheckKind::TwoStarredExpressions => false, CheckKind::UndefinedExport(_) => false, CheckKind::UndefinedLocal(_) => false, CheckKind::UndefinedName(_) => false, diff --git a/src/linter.rs b/src/linter.rs index 7ff4a9eaac..b972bf1476 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -463,6 +463,30 @@ mod tests { Ok(()) } + #[test] + fn f622() -> Result<()> { + let actual = check_path( + Path::new("./resources/test/fixtures/F622.py"), + &settings::Settings { + line_length: 88, + exclude: vec![], + select: BTreeSet::from([CheckCode::F622]), + }, + &fixer::Mode::Generate, + )?; + let expected = vec![Check { + kind: CheckKind::TwoStarredExpressions, + location: Location::new(1, 1), + fix: None, + }]; + assert_eq!(actual.len(), expected.len()); + for i in 0..actual.len() { + assert_eq!(actual[i], expected[i]); + } + + Ok(()) + } + #[test] fn f631() -> Result<()> { let mut actual = check_path( diff --git a/src/pyproject.rs b/src/pyproject.rs index 8996aad45e..d6ad365c5f 100644 --- a/src/pyproject.rs +++ b/src/pyproject.rs @@ -272,6 +272,8 @@ other-attribute = 1 CheckCode::F541, CheckCode::F601, CheckCode::F602, + CheckCode::F621, + CheckCode::F622, CheckCode::F631, CheckCode::F634, CheckCode::F704, diff --git a/src/settings.rs b/src/settings.rs index 05a505384a..579d54e785 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -57,6 +57,8 @@ impl Settings { CheckCode::F541, CheckCode::F601, CheckCode::F602, + CheckCode::F621, + CheckCode::F622, CheckCode::F631, CheckCode::F634, CheckCode::F704,