Skip to content

Commit 38049aa

Browse files
fix missing-required-imports introducing syntax error after dosctring ending with backslash (#19505)
Issue: #19498 ## Summary [missing-required-import](https://docs.astral.sh/ruff/rules/missing-required-import/) inserts the missing import on the line immediately following the last line of the docstring. However, if the dosctring is immediately followed by a continuation token (i.e. backslash) then this leads to a syntax error because Python interprets the docstring and the inserted import to be on the same line. The proposed solution in this PR is to check if the first token after a file docstring is a continuation character, and if so, to advance an additional line before inserting the missing import. ## Test Plan Added a unit test, and the following example was verified manually: Given this simple test Python file: ```python "Hello, World!"\ print(__doc__) ``` and this ruff linting configuration in the `pyproject.toml` file: ```toml [tool.ruff.lint] select = ["I"] [tool.ruff.lint.isort] required-imports = ["import sys"] ``` Without the changes in this PR, the ruff linter would try to insert the missing import in line 2, resulting in a syntax error, and report the following: `error: Fix introduced a syntax error. Reverting all changes.` With the changes in this PR, ruff correctly advances one more line before adding the missing import, resulting in the following output: ```python "Hello, World!"\ import sys print(__doc__) ``` --------- Co-authored-by: Jim Hoekstra <jim.hoekstra@pacmed.nl>
1 parent ec3d5eb commit 38049aa

5 files changed

Lines changed: 40 additions & 1 deletion

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Hello, world!"""\
2+
3+
x = 1; y = 2

crates/ruff_linter/src/importer/insertion.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,19 @@ impl<'a> Insertion<'a> {
5656
stylist: &Stylist,
5757
) -> Insertion<'static> {
5858
// Skip over any docstrings.
59-
let mut location = if let Some(location) = match_docstring_end(body) {
59+
let mut location = if let Some(mut location) = match_docstring_end(body) {
6060
// If the first token after the docstring is a semicolon, insert after the semicolon as
6161
// an inline statement.
6262
if let Some(offset) = match_semicolon(locator.after(location)) {
6363
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
6464
}
6565

66+
// If the first token after the docstring is a continuation character (i.e. "\"), advance
67+
// an additional row to prevent inserting in the same logical line.
68+
if match_continuation(locator.after(location)).is_some() {
69+
location = locator.full_line_end(location);
70+
}
71+
6672
// Otherwise, advance to the next row.
6773
locator.full_line_end(location)
6874
} else {
@@ -363,6 +369,16 @@ mod tests {
363369
Insertion::own_line("", TextSize::from(20), "\n")
364370
);
365371

372+
let contents = r#"
373+
"""Hello, world!"""\
374+
375+
"#
376+
.trim_start();
377+
assert_eq!(
378+
insert(contents)?,
379+
Insertion::own_line("", TextSize::from(22), "\n")
380+
);
381+
366382
let contents = r"
367383
x = 1
368384
"

crates/ruff_linter/src/rules/isort/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@ mod tests {
794794
#[test_case(Path::new("comments_and_newlines.py"))]
795795
#[test_case(Path::new("docstring.py"))]
796796
#[test_case(Path::new("docstring.pyi"))]
797+
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
797798
#[test_case(Path::new("docstring_only.py"))]
798799
#[test_case(Path::new("docstring_with_continuation.py"))]
799800
#[test_case(Path::new("docstring_with_semicolon.py"))]
@@ -828,6 +829,7 @@ mod tests {
828829
#[test_case(Path::new("comments_and_newlines.py"))]
829830
#[test_case(Path::new("docstring.py"))]
830831
#[test_case(Path::new("docstring.pyi"))]
832+
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
831833
#[test_case(Path::new("docstring_only.py"))]
832834
#[test_case(Path::new("docstring_with_continuation.py"))]
833835
#[test_case(Path::new("docstring_with_semicolon.py"))]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
source: crates/ruff_linter/src/rules/isort/mod.rs
3+
---
4+
docstring_followed_by_continuation.py:1:1: I002 [*] Missing required import: `from __future__ import annotations`
5+
Safe fix
6+
1 1 | """Hello, world!"""\
7+
2 2 |
8+
3 |+from __future__ import annotations
9+
3 4 | x = 1; y = 2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
source: crates/ruff_linter/src/rules/isort/mod.rs
3+
---
4+
docstring_followed_by_continuation.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
5+
Safe fix
6+
1 1 | """Hello, world!"""\
7+
2 2 |
8+
3 |+from __future__ import annotations as _annotations
9+
3 4 | x = 1; y = 2

0 commit comments

Comments
 (0)