[pylint] Convert a code keyword argument to a positional argument (PLR1722) (#16424)

The PR addresses issue #16396 .

Specifically:

- If the exit statement contains a code keyword argument, it is
converted into a positional argument.
- If retrieving the code from the exit statement is not possible, a
violation is raised without suggesting a fix.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
Vasco Schiavo 2025-03-03 15:20:57 +01:00 committed by GitHub
parent c4578162d5
commit 4d92e20e81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 195 additions and 12 deletions

View file

@ -0,0 +1,2 @@
exit(code=2)

View file

@ -0,0 +1,2 @@
code = {"code": 2}
exit(**code)

View file

@ -0,0 +1,6 @@
success = True
if success:
code = 0
else:
code = 1
exit(code)

View file

@ -0,0 +1,23 @@
def test_case_1():
# comments preserved with a positional argument
exit( # comment
1, # 2
) # 3
def test_case_2():
# comments preserved with a single keyword argument
exit( # comment
code=1, # 2
) # 3
def test_case_3():
# no diagnostic for multiple arguments
exit(2, 3, 4)
def test_case_4():
# this should now be fixable
codes = [1]
exit(*codes)

View file

@ -900,7 +900,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
pylint::rules::unnecessary_direct_lambda_call(checker, expr, func);
}
if checker.enabled(Rule::SysExitAlias) {
pylint::rules::sys_exit_alias(checker, func);
pylint::rules::sys_exit_alias(checker, call);
}
if checker.enabled(Rule::BadOpenMode) {
pylint::rules::bad_open_mode(checker, call);

View file

@ -64,6 +64,10 @@ mod tests {
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_10.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_11.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_12.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_13.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_14.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_15.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_16.py"))]
#[test_case(Rule::ContinueInFinally, Path::new("continue_in_finally.py"))]
#[test_case(Rule::GlobalStatement, Path::new("global_statement.py"))]
#[test_case(

View file

@ -1,10 +1,10 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::Expr;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
/// ## What it does
/// Checks for uses of the `exit()` and `quit()`.
@ -56,8 +56,8 @@ impl Violation for SysExitAlias {
}
/// PLR1722
pub(crate) fn sys_exit_alias(checker: &Checker, func: &Expr) {
let Some(builtin) = checker.semantic().resolve_builtin_symbol(func) else {
pub(crate) fn sys_exit_alias(checker: &Checker, call: &ExprCall) {
let Some(builtin) = checker.semantic().resolve_builtin_symbol(&call.func) else {
return;
};
if !matches!(builtin, "exit" | "quit") {
@ -67,16 +67,35 @@ pub(crate) fn sys_exit_alias(checker: &Checker, func: &Expr) {
SysExitAlias {
name: builtin.to_string(),
},
func.range(),
call.func.range(),
);
let has_star_kwargs = call
.arguments
.keywords
.iter()
.any(|kwarg| kwarg.arg.is_none());
// only one optional argument allowed, and we can't convert **kwargs
if call.arguments.len() > 1 || has_star_kwargs {
checker.report_diagnostic(diagnostic);
return;
};
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("sys", "exit"),
func.start(),
call.func.start(),
checker.semantic(),
)?;
let reference_edit = Edit::range_replacement(binding, func.range());
Ok(Fix::unsafe_edits(import_edit, [reference_edit]))
let reference_edit = Edit::range_replacement(binding, call.func.range());
let mut edits = vec![reference_edit];
if let Some(kwarg) = call.arguments.find_keyword("code") {
edits.push(Edit::range_replacement(
checker.source()[kwarg.value.range()].to_string(),
kwarg.range,
));
};
Ok(Fix::unsafe_edits(import_edit, edits))
});
checker.report_diagnostic(diagnostic);
}

View file

@ -0,0 +1,15 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
sys_exit_alias_13.py:1:1: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
1 | exit(code=2)
| ^^^^ PLR1722
|
= help: Replace `exit` with `sys.exit()`
Unsafe fix
1 |-exit(code=2)
1 |+import sys
2 |+sys.exit(2)
2 3 |

View file

@ -0,0 +1,10 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
sys_exit_alias_14.py:2:1: PLR1722 Use `sys.exit()` instead of `exit`
|
1 | code = {"code": 2}
2 | exit(**code)
| ^^^^ PLR1722
|
= help: Replace `exit` with `sys.exit()`

View file

@ -0,0 +1,21 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
sys_exit_alias_15.py:6:1: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
4 | else:
5 | code = 1
6 | exit(code)
| ^^^^ PLR1722
|
= help: Replace `exit` with `sys.exit()`
Unsafe fix
1 |+import sys
1 2 | success = True
2 3 | if success:
3 4 | code = 0
4 5 | else:
5 6 | code = 1
6 |-exit(code)
7 |+sys.exit(code)

View file

@ -0,0 +1,81 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
sys_exit_alias_16.py:3:5: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
1 | def test_case_1():
2 | # comments preserved with a positional argument
3 | exit( # comment
| ^^^^ PLR1722
4 | 1, # 2
5 | ) # 3
|
= help: Replace `exit` with `sys.exit()`
Unsafe fix
1 |+import sys
1 2 | def test_case_1():
2 3 | # comments preserved with a positional argument
3 |- exit( # comment
4 |+ sys.exit( # comment
4 5 | 1, # 2
5 6 | ) # 3
6 7 |
sys_exit_alias_16.py:10:5: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
8 | def test_case_2():
9 | # comments preserved with a single keyword argument
10 | exit( # comment
| ^^^^ PLR1722
11 | code=1, # 2
12 | ) # 3
|
= help: Replace `exit` with `sys.exit()`
Unsafe fix
1 |+import sys
1 2 | def test_case_1():
2 3 | # comments preserved with a positional argument
3 4 | exit( # comment
--------------------------------------------------------------------------------
7 8 |
8 9 | def test_case_2():
9 10 | # comments preserved with a single keyword argument
10 |- exit( # comment
11 |- code=1, # 2
11 |+ sys.exit( # comment
12 |+ 1, # 2
12 13 | ) # 3
13 14 |
14 15 |
sys_exit_alias_16.py:17:5: PLR1722 Use `sys.exit()` instead of `exit`
|
15 | def test_case_3():
16 | # no diagnostic for multiple arguments
17 | exit(2, 3, 4)
| ^^^^ PLR1722
|
= help: Replace `exit` with `sys.exit()`
sys_exit_alias_16.py:23:5: PLR1722 [*] Use `sys.exit()` instead of `exit`
|
21 | # this should now be fixable
22 | codes = [1]
23 | exit(*codes)
| ^^^^ PLR1722
|
= help: Replace `exit` with `sys.exit()`
Unsafe fix
1 |+import sys
1 2 | def test_case_1():
2 3 | # comments preserved with a positional argument
3 4 | exit( # comment
--------------------------------------------------------------------------------
20 21 | def test_case_4():
21 22 | # this should now be fixable
22 23 | codes = [1]
23 |- exit(*codes)
24 |+ sys.exit(*codes)