Skip to content
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

Add b040: exception with note added not re-raised or used (#12097) #12764

Closed
wants to merge 3 commits into from
Closed
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
196 changes: 196 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B040.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
def arbitrary_fun(*args, **kwargs): ...


def broken():
try:
...
except Exception as e:
e.add_note("...") # error

def not_broken():
try:
...
except Exception as e: # safe (other linters will catch this)
pass

def not_broken2():
try:
...
except Exception as e:
e.add_note("...")
raise # safe

def broken2():
# other exception raised
try:
...
except Exception as e:
f = ValueError()
e.add_note("...") # error
raise f

def not_broken3():
# raised as cause
try:
...
except Exception as e:
f = ValueError()
e.add_note("...") # safe
raise f from e

def not_broken4():
# assigned to other variable
try:
...
except Exception as e:
e.add_note("...") # safe
foo = e

def not_broken5():
# "used" in function call
try:
...
except Exception as e:
e.add_note("...") # safe
# not that printing the exception is actually using it, but we treat
# it being used as a parameter to any function as "using" it
print(e)

def not_broken6():
try:
...
except Exception as e:
e.add_note("...") # safe
list(e)

def not_broken7():
# kwarg
try:
...
except Exception as e:
e.add_note("...") # safe
arbitrary_fun(kwarg=e)

def not_broken8():
# stararg
try:
...
except Exception as e:
e.add_note("...") # safe
arbitrary_fun(*(e,))

def not_broken9():
try:
...
except Exception as e:
e.add_note("...") # safe
arbitrary_fun(**{"e": e})


def broken3():
# multiple ExceptHandlers
try:
...
except ValueError as e:
e.add_note("") # error
except TypeError as e:
raise e

def not_broken10():
# exception variable used before `add_note`
mylist = []
try:
...
except Exception as e: # safe
mylist.append(e)
e.add_note("")

def not_broken11():
# AnnAssign
try:
...
except Exception as e: # safe
e.add_note("")
ann_assign_target: Exception = e

def broken4():
# special case: e is only used in the `add_note` call itself
try:
...
except Exception as e: # error
e.add_note(str(e))
e.add_note(str(e))

def broken5():
# check nesting
try:
...
except Exception as e: # error
e.add_note("")
try:
...
except ValueError as e:
raise

def broken6():
# questionable if this should error
try:
...
except Exception as e:
e.add_note("")
e = ValueError()



def broken7():
exc = ValueError()
exc.add_note("")


def not_broken12(exc: ValueError):
exc.add_note("")



def not_broken13():
e2 = ValueError()
try:
...
except Exception as e:
e2.add_note(str(e)) # safe
raise e2


def broken8():
try:
...
except Exception as e: # should error
e.add_note("")
f = lambda e: e

def broken9():
try:
...
except Exception as e: # should error
e.add_note("")

def habla(e):
raise e


# *** unhandled cases ***
# We also don't track other variables
try:
...
except Exception as e:
e3 = e
e3.add_note("") # should error

# we currently only check the target of the except handler, even though this clearly
# is hitting the same general pattern. But handling this case without a lot of false
# alarms is very tricky without more infrastructure.
try:
...
except Exception as e:
e2 = ValueError() # should error
e2.add_note(str(e))
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker, stmt, body, handlers, orelse, finalbody,
);
}
if checker.enabled(Rule::SuppressedException) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule name is very close to SIM105 suppressible-exception.

Which makes me wonder if the rule should be integrated into PLW0133? @AlexWaygood what do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the name is similar, but this check is about checking if there is only add_note executed on the exception, so it's quite different from SIM105. On the other hand it is similar to PLW0133, but this rule exists in the flake8 bugbear, so it might be worth keeping the same set of rules with upstream..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integrating it into PLW0133 is appealing because it's a more general rule, and I agree that this is just trying to tackle a variant of that problem.

But flake8-bugbear is more popular and widely used than pylint, I think, and I believe people much more often select our B rules than our PLW rules. So I agree that consistency with flake8-bugbear is useful here.

I think that when doing rule recategorisation (a medium-term goal that you don't have to worry about here @divident), we'll probably no longer have categories that refer to older linters, and at that point it would probably make sense to merge PLW0133 and this rule. But for now I guess I'd go for a separate check, despite how similar the two rules are.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels strange to me to introduce a new rule if we know we'll want to merge it eventually anyway. But I do see the point and redirects are relatively cheap for users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I think we have to come up a more specific name to also make it clear that this is a very specific rule and it doesn't aim to catch all suppressed exceptions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the name "add_note_exception_suppression" ?

A few more suggestions:

  1. add_note_only_exception
  2. note_only_exception_suppression
  3. exclusive_add_note_exception
  4. exception_note_only
  5. suppressed_with_add_note

flake8_bugbear::rules::suppressed_exception(checker, handlers);
}
if checker.enabled(Rule::ReturnInTryExceptFinally) {
flake8_simplify::rules::return_in_try_except_finally(
checker, body, handlers, finalbody,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bugbear, "034") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ReSubPositionalArgs),
(Flake8Bugbear, "035") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StaticKeyDictComprehension),
(Flake8Bugbear, "039") => (RuleGroup::Preview, rules::flake8_bugbear::rules::MutableContextvarDefault),
(Flake8Bugbear, "040") => (RuleGroup::Preview, rules::flake8_bugbear::rules::SuppressedException),
(Flake8Bugbear, "901") => (RuleGroup::Preview, rules::flake8_bugbear::rules::ReturnInGenerator),
(Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept),
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ mod tests {
#[test_case(Rule::ReturnInGenerator, Path::new("B901.py"))]
#[test_case(Rule::LoopIteratorMutation, Path::new("B909.py"))]
#[test_case(Rule::MutableContextvarDefault, Path::new("B039.py"))]
#[test_case(Rule::SuppressedException, Path::new("B040.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub(crate) use setattr_with_constant::*;
pub(crate) use star_arg_unpacking_after_keyword_arg::*;
pub(crate) use static_key_dict_comprehension::*;
pub(crate) use strip_with_multi_characters::*;
pub(crate) use suppressed_exception::*;
pub(crate) use unary_prefix_increment_decrement::*;
pub(crate) use unintentional_type_annotation::*;
pub(crate) use unreliable_callable_check::*;
Expand Down Expand Up @@ -65,6 +66,7 @@ mod setattr_with_constant;
mod star_arg_unpacking_after_keyword_arg;
mod static_key_dict_comprehension;
mod strip_with_multi_characters;
mod suppressed_exception;
mod unary_prefix_increment_decrement;
mod unintentional_type_annotation;
mod unreliable_callable_check;
Expand Down
Loading
Loading