diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E40.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E40.py index 6c71fa2b0f295e..75608fca822c81 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E40.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E40.py @@ -1,5 +1,12 @@ #: E401 import os, sys +import re as regex, string # also with a comment! + +def blah(): + import datetime as dt, copy + def nested_and_tested(): + import builtins, textwrap as tw + #: Okay import os import sys diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 8475fde905e675..12bc583c208566 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -67,6 +67,7 @@ mod tests { } #[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))] + #[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))] #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))] #[test_case(Rule::TypeComparison, Path::new("E721.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/imports.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/imports.rs index b15f852605fe05..26a1022b15c26d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/imports.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/imports.rs @@ -1,6 +1,7 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{Alias, PySourceType, Stmt}; +use ruff_python_trivia::{indentation_at_offset, PythonWhitespace}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -27,10 +28,16 @@ use crate::checkers::ast::Checker; pub struct MultipleImportsOnOneLine; impl Violation for MultipleImportsOnOneLine { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Multiple imports on one line") } + + fn fix_title(&self) -> Option { + Some(format!("Split imports onto multiple lines")) + } } /// ## What it does @@ -85,9 +92,40 @@ impl Violation for ModuleImportNotAtTopOfFile { /// E401 pub(crate) fn multiple_imports_on_one_line(checker: &mut Checker, stmt: &Stmt, names: &[Alias]) { if names.len() > 1 { - checker - .diagnostics - .push(Diagnostic::new(MultipleImportsOnOneLine, stmt.range())); + let mut diagnostic = Diagnostic::new(MultipleImportsOnOneLine, stmt.range()); + + if checker.settings.preview.is_enabled() { + let indentation = indentation_at_offset(stmt.start(), checker.locator()).unwrap_or(""); + + let mut replacement = String::new(); + + for item in names { + let Alias { + range: _, + name, + asname, + } = item; + + if let Some(asname) = asname { + replacement = format!("{replacement}{indentation}import {name} as {asname}\n"); + } else { + replacement = format!("{replacement}{indentation}import {name}\n"); + } + } + + // remove leading whitespace because we start at the import keyword + replacement = replacement.trim_whitespace_start().to_string(); + + // remove trailing newline + replacement = replacement.trim_end_matches('\n').to_string(); + + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + replacement, + stmt.range(), + ))); + } + + checker.diagnostics.push(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap index 3ee33734d45f6a..4f837f609a4b7a 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E401_E40.py.snap @@ -6,8 +6,40 @@ E40.py:2:1: E401 Multiple imports on one line 1 | #: E401 2 | import os, sys | ^^^^^^^^^^^^^^ E401 -3 | #: Okay -4 | import os +3 | import re as regex, string # also with a comment! | + = help: Split imports onto multiple lines + +E40.py:3:1: E401 Multiple imports on one line + | +1 | #: E401 +2 | import os, sys +3 | import re as regex, string # also with a comment! + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401 +4 | +5 | def blah(): + | + = help: Split imports onto multiple lines + +E40.py:6:5: E401 Multiple imports on one line + | +5 | def blah(): +6 | import datetime as dt, copy + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401 +7 | def nested_and_tested(): +8 | import builtins, textwrap as tw + | + = help: Split imports onto multiple lines + +E40.py:8:9: E401 Multiple imports on one line + | + 6 | import datetime as dt, copy + 7 | def nested_and_tested(): + 8 | import builtins, textwrap as tw + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401 + 9 | +10 | #: Okay + | + = help: Split imports onto multiple lines diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap index 2525069ae9ff4b..ed35a83c23fb50 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E402_E40.py.snap @@ -1,31 +1,156 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -E40.py:55:1: E402 Module level import not at top of file +E40.py:11:1: E402 Module level import not at top of file | -53 | VERSION = '1.2.3' -54 | -55 | import foo +10 | #: Okay +11 | import os + | ^^^^^^^^^ E402 +12 | import sys + | + +E40.py:12:1: E402 Module level import not at top of file + | +10 | #: Okay +11 | import os +12 | import sys + | ^^^^^^^^^^ E402 +13 | +14 | from subprocess import Popen, PIPE + | + +E40.py:14:1: E402 Module level import not at top of file + | +12 | import sys +13 | +14 | from subprocess import Popen, PIPE + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E402 +15 | +16 | from myclass import MyClass + | + +E40.py:16:1: E402 Module level import not at top of file + | +14 | from subprocess import Popen, PIPE +15 | +16 | from myclass import MyClass + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E402 +17 | from foo.bar.yourclass import YourClass + | + +E40.py:17:1: E402 Module level import not at top of file + | +16 | from myclass import MyClass +17 | from foo.bar.yourclass import YourClass + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E402 +18 | +19 | import myclass + | + +E40.py:19:1: E402 Module level import not at top of file + | +17 | from foo.bar.yourclass import YourClass +18 | +19 | import myclass + | ^^^^^^^^^^^^^^ E402 +20 | import foo.bar.yourclass +21 | #: Okay + | + +E40.py:20:1: E402 Module level import not at top of file + | +19 | import myclass +20 | import foo.bar.yourclass + | ^^^^^^^^^^^^^^^^^^^^^^^^ E402 +21 | #: Okay +22 | __all__ = ['abc'] + | + +E40.py:24:1: E402 Module level import not at top of file + | +22 | __all__ = ['abc'] +23 | +24 | import foo + | ^^^^^^^^^^ E402 +25 | #: Okay +26 | __version__ = "42" + | + +E40.py:28:1: E402 Module level import not at top of file + | +26 | __version__ = "42" +27 | +28 | import foo + | ^^^^^^^^^^ E402 +29 | #: Okay +30 | __author__ = "Simon Gomizelj" + | + +E40.py:32:1: E402 Module level import not at top of file + | +30 | __author__ = "Simon Gomizelj" +31 | +32 | import foo + | ^^^^^^^^^^ E402 +33 | #: Okay +34 | try: + | + +E40.py:43:1: E402 Module level import not at top of file + | +41 | print('made attempt to import foo') +42 | +43 | import bar + | ^^^^^^^^^^ E402 +44 | #: Okay +45 | with warnings.catch_warnings(): + | + +E40.py:49:1: E402 Module level import not at top of file + | +47 | import foo +48 | +49 | import bar + | ^^^^^^^^^^ E402 +50 | #: Okay +51 | if False: + | + +E40.py:58:1: E402 Module level import not at top of file + | +56 | import mwahaha +57 | +58 | import bar + | ^^^^^^^^^^ E402 +59 | #: E402 +60 | VERSION = '1.2.3' + | + +E40.py:62:1: E402 Module level import not at top of file + | +60 | VERSION = '1.2.3' +61 | +62 | import foo | ^^^^^^^^^^ E402 -56 | #: E402 -57 | import foo +63 | #: E402 +64 | import foo | -E40.py:57:1: E402 Module level import not at top of file +E40.py:64:1: E402 Module level import not at top of file | -55 | import foo -56 | #: E402 -57 | import foo +62 | import foo +63 | #: E402 +64 | import foo | ^^^^^^^^^^ E402 -58 | -59 | a = 1 +65 | +66 | a = 1 | -E40.py:61:1: E402 Module level import not at top of file +E40.py:68:1: E402 Module level import not at top of file | -59 | a = 1 -60 | -61 | import bar +66 | a = 1 +67 | +68 | import bar | ^^^^^^^^^^ E402 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E401_E40.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E401_E40.py.snap new file mode 100644 index 00000000000000..7f0e97e781d71d --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E401_E40.py.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E40.py:2:1: E401 [*] Multiple imports on one line + | +1 | #: E401 +2 | import os, sys + | ^^^^^^^^^^^^^^ E401 +3 | import re as regex, string # also with a comment! + | + = help: Split imports onto multiple lines + +ℹ Safe fix +1 1 | #: E401 +2 |-import os, sys + 2 |+import os + 3 |+import sys +3 4 | import re as regex, string # also with a comment! +4 5 | +5 6 | def blah(): + +E40.py:3:1: E401 [*] Multiple imports on one line + | +1 | #: E401 +2 | import os, sys +3 | import re as regex, string # also with a comment! + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401 +4 | +5 | def blah(): + | + = help: Split imports onto multiple lines + +ℹ Safe fix +1 1 | #: E401 +2 2 | import os, sys +3 |-import re as regex, string # also with a comment! + 3 |+import re as regex + 4 |+import string # also with a comment! +4 5 | +5 6 | def blah(): +6 7 | import datetime as dt, copy + +E40.py:6:5: E401 [*] Multiple imports on one line + | +5 | def blah(): +6 | import datetime as dt, copy + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401 +7 | def nested_and_tested(): +8 | import builtins, textwrap as tw + | + = help: Split imports onto multiple lines + +ℹ Safe fix +3 3 | import re as regex, string # also with a comment! +4 4 | +5 5 | def blah(): +6 |- import datetime as dt, copy + 6 |+ import datetime as dt + 7 |+ import copy +7 8 | def nested_and_tested(): +8 9 | import builtins, textwrap as tw +9 10 | + +E40.py:8:9: E401 [*] Multiple imports on one line + | + 6 | import datetime as dt, copy + 7 | def nested_and_tested(): + 8 | import builtins, textwrap as tw + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401 + 9 | +10 | #: Okay + | + = help: Split imports onto multiple lines + +ℹ Safe fix +5 5 | def blah(): +6 6 | import datetime as dt, copy +7 7 | def nested_and_tested(): +8 |- import builtins, textwrap as tw + 8 |+ import builtins + 9 |+ import textwrap as tw +9 10 | +10 11 | #: Okay +11 12 | import os + +