diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/sys_exit_alias_12.py b/crates/ruff_linter/resources/test/fixtures/pylint/sys_exit_alias_12.py new file mode 100644 index 0000000000000..8841bb5500ba4 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/sys_exit_alias_12.py @@ -0,0 +1,3 @@ +import os \ + +exit(0) diff --git a/crates/ruff_linter/src/importer/insertion.rs b/crates/ruff_linter/src/importer/insertion.rs index 13447499f2858..c4fad038fdb2e 100644 --- a/crates/ruff_linter/src/importer/insertion.rs +++ b/crates/ruff_linter/src/importer/insertion.rs @@ -57,7 +57,7 @@ impl<'a> Insertion<'a> { let mut location = if let Some(location) = match_docstring_end(body) { // If the first token after the docstring is a semicolon, insert after the semicolon as // an inline statement. - if let Some(offset) = match_leading_semicolon(locator.after(location)) { + if let Some(offset) = match_semicolon(locator.after(location)) { return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";"); } @@ -109,10 +109,14 @@ impl<'a> Insertion<'a> { stylist: &Stylist, ) -> Insertion<'static> { let location = stmt.end(); - if let Some(offset) = match_leading_semicolon(locator.after(location)) { + if let Some(offset) = match_semicolon(locator.after(location)) { // If the first token after the statement is a semicolon, insert after the semicolon as // an inline statement. Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";") + } else if match_continuation(locator.after(location)).is_some() { + // If the first token after the statement is a continuation, insert after the statement + // with a semicolon. + Insertion::inline("; ", location, "") } else { // Otherwise, insert on the next line. Insertion::own_line( @@ -289,8 +293,8 @@ fn match_docstring_end(body: &[Stmt]) -> Option { Some(stmt.end()) } -/// If a line starts with a semicolon, return its offset. -fn match_leading_semicolon(s: &str) -> Option { +/// If the next token is a semicolon, return its offset. +fn match_semicolon(s: &str) -> Option { for (offset, c) in s.char_indices() { match c { ' ' | '\t' => continue, @@ -301,6 +305,18 @@ fn match_leading_semicolon(s: &str) -> Option { None } +/// If the next token is a continuation (`\`), return its offset. +fn match_continuation(s: &str) -> Option { + for (offset, c) in s.char_indices() { + match c { + ' ' | '\t' => continue, + '\\' => return Some(TextSize::try_from(offset).unwrap()), + _ => break, + } + } + None +} + #[cfg(test)] mod tests { use anyhow::Result; diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 2e173f6557719..aee24e4e62037 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -51,6 +51,7 @@ mod tests { #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_9.py"))] #[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::ContinueInFinally, Path::new("continue_in_finally.py"))] #[test_case(Rule::GlobalStatement, Path::new("global_statement.py"))] #[test_case( diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap new file mode 100644 index 0000000000000..24108309b0394 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +sys_exit_alias_12.py:3:1: PLR1722 [*] Use `sys.exit()` instead of `exit` + | +1 | import os \ +2 | +3 | exit(0) + | ^^^^ PLR1722 + | + = help: Replace `exit` with `sys.exit()` + +ℹ Suggested fix +1 |-import os \ + 1 |+import os; import sys \ +2 2 | +3 |-exit(0) + 3 |+sys.exit(0) + +