diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B025.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B025.py index 085f82f343c91..c2b2a1fefac87 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B025.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B025.py @@ -1,6 +1,6 @@ """ Should emit: -B025 - on lines 15, 22, 31 +B025 - on lines 15, 22, 31, 40, 47, 56 """ import pickle @@ -36,3 +36,28 @@ a = 2 except (OSError, TypeError): a = 2 + +try: + a = 1 +except* ValueError: + a = 2 +except* ValueError: + a = 2 + +try: + a = 1 +except* pickle.PickleError: + a = 2 +except* ValueError: + a = 2 +except* pickle.PickleError: + a = 2 + +try: + a = 1 +except* (ValueError, TypeError): + a = 2 +except* ValueError: + a = 2 +except* (OSError, TypeError): + a = 2 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B029.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B029.py index beb04a4059392..f30d0b387b886 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B029.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B029.py @@ -1,6 +1,6 @@ """ Should emit: -B029 - on lines 8 and 13 +B029 - on lines 8, 13, 18 and 23 """ try: @@ -11,4 +11,14 @@ try: pass except () as e: - pass \ No newline at end of file + pass + +try: + pass +except* (): + pass + +try: + pass +except* () as e: + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B030.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B030.py index 098f724249ac2..d9e2f94d23f8a 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B030.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B030.py @@ -130,3 +130,23 @@ def what_to_catch(): pass except (a, b) * (c, d): # B030 pass + +try: + pass +except* 1: # Error + pass + +try: + pass +except* (1, ValueError): # Error + pass + +try: + pass +except* (ValueError, (RuntimeError, (KeyError, TypeError))): # Error + pass + +try: + pass +except* (a, b) * (c, d): # B030 + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B904.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B904.py index ec4bc9e6f3a12..d752aeca6e9d6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B904.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B904.py @@ -1,6 +1,6 @@ """ Should emit: -B904 - on lines 10, 11, 16, 62, and 64 +B904 - on lines 10, 11, 16, 62, 64, 79, 81, 87, 88, 93 and 97 """ try: @@ -71,3 +71,29 @@ def context_switch(): match 0: case 0: raise RuntimeError("boom!") + +try: + ... +except* Exception as e: + if ...: + raise RuntimeError("boom!") + else: + raise RuntimeError("bang!") + +try: + raise ValueError +except* ValueError: + if "abc": + raise TypeError + raise UserWarning +except* AssertionError: + raise # Bare `raise` should not be an error +except* Exception as err: + assert err + raise Exception("No cause here...") +except* BaseException as err: + raise err +except* BaseException as err: + raise some_other_err +finally: + raise Exception("Nothing to chain from, so no warning here") diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index bff4c8ea88832..976fe26d31810 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -42,13 +42,18 @@ use crate::registry::Rule; #[derive(ViolationMetadata)] pub(crate) struct DuplicateTryBlockException { name: String, + is_star: bool, } impl Violation for DuplicateTryBlockException { #[derive_message_formats] fn message(&self) -> String { - let DuplicateTryBlockException { name } = self; - format!("try-except block with duplicate exception `{name}`") + let DuplicateTryBlockException { name, is_star } = self; + if *is_star { + format!("try-except* block with duplicate exception `{name}`") + } else { + format!("try-except block with duplicate exception `{name}`") + } } } @@ -207,9 +212,15 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand if checker.enabled(Rule::DuplicateTryBlockException) { for (name, exprs) in duplicates { for expr in exprs { + let is_star = checker + .semantic() + .current_statement() + .as_try_stmt() + .is_some_and(|try_stmt| try_stmt.is_star); checker.diagnostics.push(Diagnostic::new( DuplicateTryBlockException { name: name.segments().join("."), + is_star, }, expr.range(), )); diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index 1e55a6abde3b8..d43fc889e211b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -34,13 +34,18 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[derive(ViolationMetadata)] -pub(crate) struct ExceptWithEmptyTuple; +pub(crate) struct ExceptWithEmptyTuple { + is_star: bool, +} impl Violation for ExceptWithEmptyTuple { #[derive_message_formats] fn message(&self) -> String { - "Using `except ():` with an empty tuple does not catch anything; add exceptions to handle" - .to_string() + if self.is_star { + "Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle".to_string() + } else { + "Using `except ():` with an empty tuple does not catch anything; add exceptions to handle".to_string() + } } } @@ -54,9 +59,15 @@ pub(crate) fn except_with_empty_tuple(checker: &mut Checker, except_handler: &Ex let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else { return; }; + if elts.is_empty() { + let is_star = checker + .semantic() + .current_statement() + .as_try_stmt() + .is_some_and(|try_stmt| try_stmt.is_star); checker.diagnostics.push(Diagnostic::new( - ExceptWithEmptyTuple, + ExceptWithEmptyTuple { is_star }, except_handler.range(), )); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs index 180596395846c..13f9f544fa00f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs @@ -35,13 +35,20 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) /// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) #[derive(ViolationMetadata)] -pub(crate) struct ExceptWithNonExceptionClasses; +pub(crate) struct ExceptWithNonExceptionClasses { + is_star: bool, +} impl Violation for ExceptWithNonExceptionClasses { #[derive_message_formats] fn message(&self) -> String { - "`except` handlers should only be exception classes or tuples of exception classes" - .to_string() + if self.is_star { + "`except*` handlers should only be exception classes or tuples of exception classes" + .to_string() + } else { + "`except` handlers should only be exception classes or tuples of exception classes" + .to_string() + } } } @@ -60,9 +67,15 @@ pub(crate) fn except_with_non_exception_classes( expr, Expr::Subscript(_) | Expr::Attribute(_) | Expr::Name(_) | Expr::Call(_), ) { - checker - .diagnostics - .push(Diagnostic::new(ExceptWithNonExceptionClasses, expr.range())); + let is_star = checker + .semantic() + .current_statement() + .as_try_stmt() + .is_some_and(|try_stmt| try_stmt.is_star); + checker.diagnostics.push(Diagnostic::new( + ExceptWithNonExceptionClasses { is_star }, + expr.range(), + )); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs index 493cd06f72c3e..b989606154da0 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs @@ -47,14 +47,22 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[derive(ViolationMetadata)] -pub(crate) struct RaiseWithoutFromInsideExcept; +pub(crate) struct RaiseWithoutFromInsideExcept { + is_star: bool, +} impl Violation for RaiseWithoutFromInsideExcept { #[derive_message_formats] fn message(&self) -> String { - "Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... \ - from None` to distinguish them from errors in exception handling" - .to_string() + if self.is_star { + "Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... \ + from None` to distinguish them from errors in exception handling" + .to_string() + } else { + "Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... \ + from None` to distinguish them from errors in exception handling" + .to_string() + } } } @@ -92,9 +100,16 @@ pub(crate) fn raise_without_from_inside_except( } } - checker - .diagnostics - .push(Diagnostic::new(RaiseWithoutFromInsideExcept, range)); + let is_star = checker + .semantic() + .current_statement() + .as_try_stmt() + .is_some_and(|try_stmt| try_stmt.is_star); + + checker.diagnostics.push(Diagnostic::new( + RaiseWithoutFromInsideExcept { is_star }, + range, + )); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B025_B025.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B025_B025.py.snap index 802a09d01dea0..117f480fc15da 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B025_B025.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B025_B025.py.snap @@ -38,3 +38,40 @@ B025.py:37:18: B025 try-except block with duplicate exception `TypeError` | ^^^^^^^^^ B025 38 | a = 2 | + +B025.py:44:9: B025 try-except* block with duplicate exception `ValueError` + | +42 | except* ValueError: +43 | a = 2 +44 | except* ValueError: + | ^^^^^^^^^^ B025 +45 | a = 2 + | + +B025.py:53:9: B025 try-except* block with duplicate exception `pickle.PickleError` + | +51 | except* ValueError: +52 | a = 2 +53 | except* pickle.PickleError: + | ^^^^^^^^^^^^^^^^^^ B025 +54 | a = 2 + | + +B025.py:60:9: B025 try-except* block with duplicate exception `ValueError` + | +58 | except* (ValueError, TypeError): +59 | a = 2 +60 | except* ValueError: + | ^^^^^^^^^^ B025 +61 | a = 2 +62 | except* (OSError, TypeError): + | + +B025.py:62:19: B025 try-except* block with duplicate exception `TypeError` + | +60 | except* ValueError: +61 | a = 2 +62 | except* (OSError, TypeError): + | ^^^^^^^^^ B025 +63 | a = 2 + | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap index 0417f6e65b9b7..57ecefcc16353 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B029_B029.py.snap @@ -20,4 +20,26 @@ B029.py:13:1: B029 Using `except ():` with an empty tuple does not catch anythin 13 | / except () as e: 14 | | pass | |________^ B029 +15 | +16 | try: + | + +B029.py:18:1: B029 Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle + | +16 | try: +17 | pass +18 | / except* (): +19 | | pass + | |________^ B029 +20 | +21 | try: + | + +B029.py:23:1: B029 Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle + | +21 | try: +22 | pass +23 | / except* () as e: +24 | | pass + | |________^ B029 | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B030_B030.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B030_B030.py.snap index a182032f77343..c105eb23a028e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B030_B030.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B030_B030.py.snap @@ -47,7 +47,7 @@ B030.py:33:29: B030 `except` handlers should only be exception classes or tuples 34 | pass | -B030.py:39:28: B030 `except` handlers should only be exception classes or tuples of exception classes +B030.py:39:28: B030 `except*` handlers should only be exception classes or tuples of exception classes | 37 | try: 38 | pass @@ -64,3 +64,39 @@ B030.py:131:8: B030 `except` handlers should only be exception classes or tuples | ^^^^^^^^^^^^^^^ B030 132 | pass | + +B030.py:136:9: B030 `except*` handlers should only be exception classes or tuples of exception classes + | +134 | try: +135 | pass +136 | except* 1: # Error + | ^ B030 +137 | pass + | + +B030.py:141:10: B030 `except*` handlers should only be exception classes or tuples of exception classes + | +139 | try: +140 | pass +141 | except* (1, ValueError): # Error + | ^ B030 +142 | pass + | + +B030.py:146:22: B030 `except*` handlers should only be exception classes or tuples of exception classes + | +144 | try: +145 | pass +146 | except* (ValueError, (RuntimeError, (KeyError, TypeError))): # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B030 +147 | pass + | + +B030.py:151:9: B030 `except*` handlers should only be exception classes or tuples of exception classes + | +149 | try: +150 | pass +151 | except* (a, b) * (c, d): # B030 + | ^^^^^^^^^^^^^^^ B030 +152 | pass + | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap index 4b46459c289f7..dc4604c1bf312 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B904_B904.py.snap @@ -66,4 +66,66 @@ B904.py:73:13: B904 Within an `except` clause, raise exceptions with `raise ... 72 | case 0: 73 | raise RuntimeError("boom!") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904 +74 | +75 | try: + | + +B904.py:79:9: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + | +77 | except* Exception as e: +78 | if ...: +79 | raise RuntimeError("boom!") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904 +80 | else: +81 | raise RuntimeError("bang!") + | + +B904.py:81:9: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + | +79 | raise RuntimeError("boom!") +80 | else: +81 | raise RuntimeError("bang!") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904 +82 | +83 | try: + | + +B904.py:87:9: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + | +85 | except* ValueError: +86 | if "abc": +87 | raise TypeError + | ^^^^^^^^^^^^^^^ B904 +88 | raise UserWarning +89 | except* AssertionError: + | + +B904.py:88:5: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + | +86 | if "abc": +87 | raise TypeError +88 | raise UserWarning + | ^^^^^^^^^^^^^^^^^ B904 +89 | except* AssertionError: +90 | raise # Bare `raise` should not be an error + | + +B904.py:93:5: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + | +91 | except* Exception as err: +92 | assert err +93 | raise Exception("No cause here...") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B904 +94 | except* BaseException as err: +95 | raise err + | + +B904.py:97:5: B904 Within an `except*` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + | +95 | raise err +96 | except* BaseException as err: +97 | raise some_other_err + | ^^^^^^^^^^^^^^^^^^^^ B904 +98 | finally: +99 | raise Exception("Nothing to chain from, so no warning here") |