[tryceratops] Add fix for error-instead-of-exception (TRY400) (#9520)

## Summary

add autofix for `error-instead-of-exception` (`TRY400`)

## Test Plan

`cargo test`
This commit is contained in:
Steve C
2024-01-15 22:00:04 -05:00
committed by GitHub
parent 2bddde2627
commit 7ef7e0ddb6
5 changed files with 316 additions and 6 deletions

View File

@@ -11,6 +11,8 @@ mod tests {
use test_case::test_case;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::{assert_messages, settings};
@@ -33,4 +35,22 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::ErrorInsteadOfException, Path::new("TRY400.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("tryceratops").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,12 +1,13 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, ExceptHandler};
use ruff_python_ast::{self as ast, ExceptHandler, Expr};
use ruff_python_semantic::analyze::logging::exc_info;
use ruff_python_stdlib::logging::LoggingLevel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::rules::tryceratops::helpers::LoggerCandidateVisitor;
/// ## What it does
@@ -42,16 +43,28 @@ use crate::rules::tryceratops::helpers::LoggerCandidateVisitor;
/// logging.exception("Exception occurred")
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as safe when run against `logging.error` calls,
/// but unsafe when marked against other logger-like calls (e.g.,
/// `logger.error`), since the rule is prone to false positives when detecting
/// logger-like calls outside of the `logging` module.
///
/// ## References
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
#[violation]
pub struct ErrorInsteadOfException;
impl Violation for ErrorInsteadOfException {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `logging.exception` instead of `logging.error`")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Replace with `exception`"))
}
}
/// TRY400
@@ -67,9 +80,60 @@ pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[Exce
for (expr, logging_level) in calls {
if matches!(logging_level, LoggingLevel::Error) {
if exc_info(&expr.arguments, checker.semantic()).is_none() {
checker
.diagnostics
.push(Diagnostic::new(ErrorInsteadOfException, expr.range()));
let mut diagnostic = Diagnostic::new(ErrorInsteadOfException, expr.range());
if checker.settings.preview.is_enabled() {
match expr.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement("exception".to_string(), attr.range()),
// When run against `logging.error`, the fix is safe; otherwise,
// the object _may_ not be a logger.
if checker
.semantic()
.resolve_call_path(expr.func.as_ref())
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["logging", "error"])
})
{
Applicability::Safe
} else {
Applicability::Unsafe
},
));
}
Expr::Name(_) => {
diagnostic.try_set_fix(|| {
let (import_edit, binding) =
checker.importer().get_or_import_symbol(
&ImportRequest::import("logging", "exception"),
expr.start(),
checker.semantic(),
)?;
let name_edit =
Edit::range_replacement(binding, expr.func.range());
Ok(Fix::applicable_edits(
import_edit,
[name_edit],
// When run against `logging.error`, the fix is safe; otherwise,
// the object _may_ not be a logger.
if checker
.semantic()
.resolve_call_path(expr.func.as_ref())
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["logging", "error"])
})
{
Applicability::Safe
} else {
Applicability::Unsafe
},
))
});
}
_ => {}
}
}
checker.diagnostics.push(diagnostic);
}
}
}

View File

@@ -10,6 +10,7 @@ TRY400.py:16:9: TRY400 Use `logging.exception` instead of `logging.error`
17 |
18 | if True:
|
= help: Replace with `exception`
TRY400.py:19:13: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -17,6 +18,7 @@ TRY400.py:19:13: TRY400 Use `logging.exception` instead of `logging.error`
19 | logging.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
TRY400.py:26:9: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -27,6 +29,7 @@ TRY400.py:26:9: TRY400 Use `logging.exception` instead of `logging.error`
27 |
28 | if True:
|
= help: Replace with `exception`
TRY400.py:29:13: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -34,6 +37,7 @@ TRY400.py:29:13: TRY400 Use `logging.exception` instead of `logging.error`
29 | logger.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
TRY400.py:36:9: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -44,6 +48,7 @@ TRY400.py:36:9: TRY400 Use `logging.exception` instead of `logging.error`
37 |
38 | if True:
|
= help: Replace with `exception`
TRY400.py:39:13: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -51,6 +56,7 @@ TRY400.py:39:13: TRY400 Use `logging.exception` instead of `logging.error`
39 | log.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
TRY400.py:46:9: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -61,6 +67,7 @@ TRY400.py:46:9: TRY400 Use `logging.exception` instead of `logging.error`
47 |
48 | if True:
|
= help: Replace with `exception`
TRY400.py:49:13: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -68,6 +75,7 @@ TRY400.py:49:13: TRY400 Use `logging.exception` instead of `logging.error`
49 | self.logger.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
TRY400.py:87:9: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -78,6 +86,7 @@ TRY400.py:87:9: TRY400 Use `logging.exception` instead of `logging.error`
88 |
89 | if True:
|
= help: Replace with `exception`
TRY400.py:90:13: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -85,6 +94,7 @@ TRY400.py:90:13: TRY400 Use `logging.exception` instead of `logging.error`
90 | error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
TRY400.py:121:13: TRY400 Use `logging.exception` instead of `logging.error`
|
@@ -93,5 +103,6 @@ TRY400.py:121:13: TRY400 Use `logging.exception` instead of `logging.error`
121 | error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`

View File

@@ -0,0 +1,215 @@
---
source: crates/ruff_linter/src/rules/tryceratops/mod.rs
---
TRY400.py:16:9: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
14 | a = 1
15 | except Exception:
16 | logging.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
17 |
18 | if True:
|
= help: Replace with `exception`
Safe fix
13 13 | try:
14 14 | a = 1
15 15 | except Exception:
16 |- logging.error("Context message here")
16 |+ logging.exception("Context message here")
17 17 |
18 18 | if True:
19 19 | logging.error("Context message here")
TRY400.py:19:13: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
18 | if True:
19 | logging.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
Safe fix
16 16 | logging.error("Context message here")
17 17 |
18 18 | if True:
19 |- logging.error("Context message here")
19 |+ logging.exception("Context message here")
20 20 |
21 21 |
22 22 | def bad():
TRY400.py:26:9: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
24 | a = 1
25 | except Exception:
26 | logger.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
27 |
28 | if True:
|
= help: Replace with `exception`
Unsafe fix
23 23 | try:
24 24 | a = 1
25 25 | except Exception:
26 |- logger.error("Context message here")
26 |+ logger.exception("Context message here")
27 27 |
28 28 | if True:
29 29 | logger.error("Context message here")
TRY400.py:29:13: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
28 | if True:
29 | logger.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
Unsafe fix
26 26 | logger.error("Context message here")
27 27 |
28 28 | if True:
29 |- logger.error("Context message here")
29 |+ logger.exception("Context message here")
30 30 |
31 31 |
32 32 | def bad():
TRY400.py:36:9: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
34 | a = 1
35 | except Exception:
36 | log.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
37 |
38 | if True:
|
= help: Replace with `exception`
Unsafe fix
33 33 | try:
34 34 | a = 1
35 35 | except Exception:
36 |- log.error("Context message here")
36 |+ log.exception("Context message here")
37 37 |
38 38 | if True:
39 39 | log.error("Context message here")
TRY400.py:39:13: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
38 | if True:
39 | log.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
Unsafe fix
36 36 | log.error("Context message here")
37 37 |
38 38 | if True:
39 |- log.error("Context message here")
39 |+ log.exception("Context message here")
40 40 |
41 41 |
42 42 | def bad():
TRY400.py:46:9: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
44 | a = 1
45 | except Exception:
46 | self.logger.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
47 |
48 | if True:
|
= help: Replace with `exception`
Unsafe fix
43 43 | try:
44 44 | a = 1
45 45 | except Exception:
46 |- self.logger.error("Context message here")
46 |+ self.logger.exception("Context message here")
47 47 |
48 48 | if True:
49 49 | self.logger.error("Context message here")
TRY400.py:49:13: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
48 | if True:
49 | self.logger.error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
Unsafe fix
46 46 | self.logger.error("Context message here")
47 47 |
48 48 | if True:
49 |- self.logger.error("Context message here")
49 |+ self.logger.exception("Context message here")
50 50 |
51 51 |
52 52 | def good():
TRY400.py:87:9: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
85 | a = 1
86 | except Exception:
87 | error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
88 |
89 | if True:
|
= help: Replace with `exception`
Safe fix
84 84 | try:
85 85 | a = 1
86 86 | except Exception:
87 |- error("Context message here")
87 |+ exception("Context message here")
88 88 |
89 89 | if True:
90 90 | error("Context message here")
TRY400.py:90:13: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
89 | if True:
90 | error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
Safe fix
87 87 | error("Context message here")
88 88 |
89 89 | if True:
90 |- error("Context message here")
90 |+ exception("Context message here")
91 91 |
92 92 |
93 93 | def good():
TRY400.py:121:13: TRY400 [*] Use `logging.exception` instead of `logging.error`
|
119 | b = 2
120 | except Exception:
121 | error("Context message here")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRY400
|
= help: Replace with `exception`
Safe fix
118 118 | try:
119 119 | b = 2
120 120 | except Exception:
121 |- error("Context message here")
121 |+ exception("Context message here")

View File

@@ -64,7 +64,7 @@ pub fn is_logger_candidate(
false
}
/// If the keywords to a logging call contain `exc_info=True` or `exc_info=sys.exc_info()`,
/// If the keywords to a logging call contain `exc_info=True` or `exc_info=sys.exc_info()`,
/// return the `Keyword` for `exc_info`.
pub fn exc_info<'a>(arguments: &'a Arguments, semantic: &SemanticModel) -> Option<&'a Keyword> {
let exc_info = arguments.find_keyword("exc_info")?;