Avoid inserting imports directly after continuation (#7553)

## Summary

This is extremely rare in practice, but common in the fuzzer issues so
worth fixing quickly.

Closes https://github.com/astral-sh/ruff/issues/7199.
This commit is contained in:
Charlie Marsh 2023-09-20 17:26:48 -04:00 committed by GitHub
parent ee9ee005c5
commit a0917ec658
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 44 additions and 4 deletions

View file

@ -0,0 +1,3 @@
import os \
exit(0)

View file

@ -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<TextSize> {
Some(stmt.end())
}
/// If a line starts with a semicolon, return its offset.
fn match_leading_semicolon(s: &str) -> Option<TextSize> {
/// If the next token is a semicolon, return its offset.
fn match_semicolon(s: &str) -> Option<TextSize> {
for (offset, c) in s.char_indices() {
match c {
' ' | '\t' => continue,
@ -301,6 +305,18 @@ fn match_leading_semicolon(s: &str) -> Option<TextSize> {
None
}
/// If the next token is a continuation (`\`), return its offset.
fn match_continuation(s: &str) -> Option<TextSize> {
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;

View file

@ -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(

View file

@ -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)