From 46ab9dec181f7736a0d32be4bad59d00228c1819 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 5 Mar 2024 11:09:15 +0100 Subject: [PATCH] [`pycodestyle`] Respect `isort` settings in blank line rules (`E3*`) (#10096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR changes the `E3*` rules to respect the `isort` `lines-after-imports` and `lines-between-types` settings. Specifically, the following rules required changing * `TooManyBlannkLines` : Respects both settings. * `BlankLinesTopLevel`: Respects `lines-after-imports`. Doesn't need to respect `lines-between-types` because it only applies to classes and functions The downside of this approach is that `isort` and the blank line rules emit a diagnostic when there are too many blank lines. The fixes aren't identical, the blank line is less opinionated, but blank lines accepts the fix of `isort`.
Outdated approach Fixes https://github.com/astral-sh/ruff/issues/10077#issuecomment-1961266981 This PR changes the blank line rules to not enforce the number of blank lines after imports (top-level) if isort is enabled and leave it to isort to enforce the right number of lines (depends on the `isort.lines-after-imports` and `isort.lines-between-types` settings). The reason to give `isort` precedence over the blank line rules is that they are configurable. Users that always want to blank lines after imports can use `isort.lines-after-imports=2` to enforce that (specifically for imports). This PR does not fix the incompatibility with the formatter in pyi files that only uses 0 to 1 blank lines. I'll address this separately.
## Review The first commit is a small refactor that simplified implementing the fix (and makes it easier to reason about what's mutable and what's not). ## Test Plan I added a new test and verified that it fails with an error that the fix never converges. I verified the snapshot output after implementing the fix. --------- Co-authored-by: Hoël Bagard <34478245+hoel-bagard@users.noreply.github.com> --- .../test/fixtures/pycodestyle/.editorconfig | 8 + .../test/fixtures/pycodestyle/E30_isort.py | 62 ++ crates/ruff_linter/src/checkers/tokens.rs | 9 +- .../ruff_linter/src/rules/pycodestyle/mod.rs | 73 ++- .../rules/pycodestyle/rules/blank_lines.rs | 558 ++++++++++-------- ...ules__pycodestyle__tests__E303_E30.py.snap | 4 +- ...patibility-lines-after(-1)-between(0).snap | 140 +++++ ...mpatibility-lines-after(0)-between(0).snap | 127 ++++ ...mpatibility-lines-after(1)-between(1).snap | 110 ++++ ...mpatibility-lines-after(4)-between(4).snap | 166 ++++++ ...patibility-lines-after(-1)-between(0).snap | 135 +++++ ...mpatibility-lines-after(0)-between(0).snap | 171 ++++++ ...mpatibility-lines-after(1)-between(1).snap | 137 +++++ ...mpatibility-lines-after(4)-between(4).snap | 109 ++++ crates/ruff_python_codegen/src/stylist.rs | 3 +- crates/ruff_source_file/src/locator.rs | 1 + 16 files changed, 1565 insertions(+), 248 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/.editorconfig create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.py create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/.editorconfig b/crates/ruff_linter/resources/test/fixtures/pycodestyle/.editorconfig new file mode 100644 index 0000000000000..8f3d733c53287 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/.editorconfig @@ -0,0 +1,8 @@ +# These rules test for intentional "odd" formatting. Using a formatter fixes that +[E{1,2,3}*.py] +generated_code = true +ij_formatter_enabled = false + +[W*.py] +generated_code = true +ij_formatter_enabled = false diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.py new file mode 100644 index 0000000000000..007097427b016 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30_isort.py @@ -0,0 +1,62 @@ +import json + + + +from typing import Any, Sequence + + +class MissingCommand(TypeError): ... # noqa: N818 + + +class BackendProxy: + backend_module: str + backend_object: str | None + backend: Any + + +if __name__ == "__main__": + import abcd + + + abcd.foo() + +def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + +if TYPE_CHECKING: + import os + + + + from typing_extensions import TypeAlias + + + abcd.foo() + +def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + ... + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: + ... + + +def _exit(self) -> None: ... + + +def _optional_commands(self) -> dict[str, bool]: ... + + +def run(argv: Sequence[str]) -> int: ... + + +def read_line(fd: int = 0) -> bytearray: ... + + +def flush() -> None: ... + + +from typing import Any, Sequence + +class MissingCommand(TypeError): ... # noqa: N818 diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index c676645815669..dea5c8d8a9e4d 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -41,14 +41,7 @@ pub(crate) fn check_tokens( Rule::BlankLinesAfterFunctionOrClass, Rule::BlankLinesBeforeNestedDefinition, ]) { - let mut blank_lines_checker = BlankLinesChecker::default(); - blank_lines_checker.check_lines( - tokens, - locator, - stylist, - settings.tab_size, - &mut diagnostics, - ); + BlankLinesChecker::new(locator, stylist, settings).check_lines(tokens, &mut diagnostics); } if settings.rules.enabled(Rule::BlanketNOQA) { diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 34b63a26ffbe9..cf2e002f492d8 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -16,7 +16,7 @@ mod tests { use crate::line_width::LineLength; use crate::registry::Rule; - use crate::rules::pycodestyle; + use crate::rules::{isort, pycodestyle}; use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -138,14 +138,23 @@ mod tests { Path::new("E25.py") )] #[test_case(Rule::MissingWhitespaceAroundParameterEquals, Path::new("E25.py"))] + fn logical(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("pycodestyle").join(path).as_path(), + &settings::LinterSettings::for_rule(rule_code), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Rule::BlankLineBetweenMethods, Path::new("E30.py"))] #[test_case(Rule::BlankLinesTopLevel, Path::new("E30.py"))] #[test_case(Rule::TooManyBlankLines, Path::new("E30.py"))] #[test_case(Rule::BlankLineAfterDecorator, Path::new("E30.py"))] #[test_case(Rule::BlankLinesAfterFunctionOrClass, Path::new("E30.py"))] #[test_case(Rule::BlankLinesBeforeNestedDefinition, Path::new("E30.py"))] - - fn logical(rule_code: Rule, path: &Path) -> Result<()> { + fn blank_lines(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("pycodestyle").join(path).as_path(), @@ -155,6 +164,64 @@ mod tests { Ok(()) } + /// Tests the compatibility of the blank line top level rule and isort. + #[test_case(-1, 0)] + #[test_case(1, 1)] + #[test_case(0, 0)] + #[test_case(4, 4)] + fn blank_lines_top_level_isort_compatibility( + lines_after_imports: isize, + lines_between_types: usize, + ) -> Result<()> { + let snapshot = format!( + "blank_lines_top_level_isort_compatibility-lines-after({lines_after_imports})-between({lines_between_types})" + ); + let diagnostics = test_path( + Path::new("pycodestyle").join("E30_isort.py"), + &settings::LinterSettings { + isort: isort::settings::Settings { + lines_after_imports, + lines_between_types, + ..isort::settings::Settings::default() + }, + ..settings::LinterSettings::for_rules([ + Rule::BlankLinesTopLevel, + Rule::UnsortedImports, + ]) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + + /// Tests the compatibility of the blank line too many lines and isort. + #[test_case(-1, 0)] + #[test_case(1, 1)] + #[test_case(0, 0)] + #[test_case(4, 4)] + fn too_many_blank_lines_isort_compatibility( + lines_after_imports: isize, + lines_between_types: usize, + ) -> Result<()> { + let snapshot = format!("too_many_blank_lines_isort_compatibility-lines-after({lines_after_imports})-between({lines_between_types})"); + let diagnostics = test_path( + Path::new("pycodestyle").join("E30_isort.py"), + &settings::LinterSettings { + isort: isort::settings::Settings { + lines_after_imports, + lines_between_types, + ..isort::settings::Settings::default() + }, + ..settings::LinterSettings::for_rules([ + Rule::TooManyBlankLines, + Rule::UnsortedImports, + ]) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test] fn constant_literals() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs index c1a95551d7040..9120c4d3ffff1 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -24,7 +24,7 @@ use ruff_python_trivia::PythonWhitespace; /// Number of blank lines around top level classes and functions. const BLANK_LINES_TOP_LEVEL: u32 = 2; /// Number of blank lines around methods and nested classes and functions. -const BLANK_LINES_METHOD_LEVEL: u32 = 1; +const BLANK_LINES_NESTED_LEVEL: u32 = 1; /// ## What it does /// Checks for missing blank lines between methods of a class. @@ -60,7 +60,7 @@ pub struct BlankLineBetweenMethods; impl AlwaysFixableViolation for BlankLineBetweenMethods { #[derive_message_formats] fn message(&self) -> String { - format!("Expected {BLANK_LINES_METHOD_LEVEL:?} blank line, found 0") + format!("Expected {BLANK_LINES_NESTED_LEVEL:?} blank line, found 0") } fn fix_title(&self) -> String { @@ -74,6 +74,10 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods { /// ## Why is this bad? /// PEP 8 recommends exactly two blank lines between top level functions and classes. /// +/// Note: The rule respects the [`lint.isort.lines-after-imports`] setting when determining +/// the required number of blank lines between top-level `import` statements and function or class definitions +/// for compatibility with isort. +/// /// ## Example /// ```python /// def func1(): @@ -98,16 +102,18 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods { #[violation] pub struct BlankLinesTopLevel { actual_blank_lines: u32, + expected_blank_lines: u32, } impl AlwaysFixableViolation for BlankLinesTopLevel { #[derive_message_formats] fn message(&self) -> String { let BlankLinesTopLevel { - actual_blank_lines: nb_blank_lines, + actual_blank_lines, + expected_blank_lines, } = self; - format!("Expected {BLANK_LINES_TOP_LEVEL:?} blank lines, found {nb_blank_lines}") + format!("Expected {expected_blank_lines:?} blank lines, found {actual_blank_lines}") } fn fix_title(&self) -> String { @@ -144,6 +150,10 @@ impl AlwaysFixableViolation for BlankLinesTopLevel { /// pass /// ``` /// +/// Note: The rule respects the following `isort` settings when determining the maximum number of blank lines allowed between two statements: +/// * [`lint.isort.lines-after-imports`]: For top-level statements directly following an import statement. +/// * [`lint.isort.lines-between-types`]: For `import` statements directly following a `from ... import ...` statement or vice versa. +/// /// ## References /// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html) @@ -155,10 +165,9 @@ pub struct TooManyBlankLines { impl AlwaysFixableViolation for TooManyBlankLines { #[derive_message_formats] fn message(&self) -> String { - let TooManyBlankLines { - actual_blank_lines: nb_blank_lines, - } = self; - format!("Too many blank lines ({nb_blank_lines})") + let TooManyBlankLines { actual_blank_lines } = self; + + format!("Too many blank lines ({actual_blank_lines})") } fn fix_title(&self) -> String { @@ -415,6 +424,8 @@ impl<'a> Iterator for LinePreprocessor<'a> { { LogicalLineKind::Function } + TokenKind::Import => LogicalLineKind::Import, + TokenKind::From => LogicalLineKind::FromImport, _ => LogicalLineKind::Other, }; @@ -560,9 +571,17 @@ enum Follows { Other, Decorator, Def, + Import, + FromImport, Docstring, } +impl Follows { + const fn is_any_import(self) -> bool { + matches!(self, Follows::Import | Follows::FromImport) + } +} + #[derive(Copy, Clone, Debug, Default)] enum Status { /// Stores the indent level where the nesting started. @@ -602,40 +621,99 @@ impl Status { } /// Contains variables used for the linting of blank lines. -#[derive(Debug, Default)] -pub(crate) struct BlankLinesChecker { - follows: Follows, - fn_status: Status, - class_status: Status, - /// First line that is not a comment. - is_not_first_logical_line: bool, - /// Used for the fix in case a comment separates two non-comment logical lines to make the comment "stick" - /// to the second line instead of the first. - last_non_comment_line_end: TextSize, - previous_unindented_line_kind: Option, +#[derive(Debug)] +pub(crate) struct BlankLinesChecker<'a> { + stylist: &'a Stylist<'a>, + locator: &'a Locator<'a>, + indent_width: IndentWidth, + lines_after_imports: isize, + lines_between_types: usize, } -impl BlankLinesChecker { +impl<'a> BlankLinesChecker<'a> { + pub(crate) fn new( + locator: &'a Locator<'a>, + stylist: &'a Stylist<'a>, + settings: &crate::settings::LinterSettings, + ) -> BlankLinesChecker<'a> { + BlankLinesChecker { + stylist, + locator, + indent_width: settings.tab_size, + lines_after_imports: settings.isort.lines_after_imports, + lines_between_types: settings.isort.lines_between_types, + } + } + /// E301, E302, E303, E304, E305, E306 - pub(crate) fn check_lines( - &mut self, - tokens: &[LexResult], - locator: &Locator, - stylist: &Stylist, - indent_width: IndentWidth, - diagnostics: &mut Vec, - ) { + pub(crate) fn check_lines(&self, tokens: &[LexResult], diagnostics: &mut Vec) { let mut prev_indent_length: Option = None; - let line_preprocessor = LinePreprocessor::new(tokens, locator, indent_width); + let mut state = BlankLinesState::default(); + let line_preprocessor = LinePreprocessor::new(tokens, self.locator, self.indent_width); for logical_line in line_preprocessor { - self.check_line( - &logical_line, - prev_indent_length, - locator, - stylist, - diagnostics, - ); + // Reset `follows` after a dedent: + // ```python + // if True: + // import test + // a = 10 + // ``` + // The `a` statement doesn't follow the `import` statement but the `if` statement. + if let Some(prev_indent_length) = prev_indent_length { + if prev_indent_length > logical_line.indent_length { + state.follows = Follows::Other; + } + } + + state.class_status.update(&logical_line); + state.fn_status.update(&logical_line); + + if state.is_not_first_logical_line { + self.check_line(&logical_line, &state, prev_indent_length, diagnostics); + } + + match logical_line.kind { + LogicalLineKind::Class => { + if matches!(state.class_status, Status::Outside) { + state.class_status = Status::Inside(logical_line.indent_length); + } + state.follows = Follows::Other; + } + LogicalLineKind::Decorator => { + state.follows = Follows::Decorator; + } + LogicalLineKind::Function => { + if matches!(state.fn_status, Status::Outside) { + state.fn_status = Status::Inside(logical_line.indent_length); + } + state.follows = Follows::Def; + } + LogicalLineKind::Comment => {} + LogicalLineKind::Import => { + state.follows = Follows::Import; + } + LogicalLineKind::FromImport => { + state.follows = Follows::FromImport; + } + LogicalLineKind::Other => { + state.follows = Follows::Other; + } + } + + if logical_line.is_docstring { + state.follows = Follows::Docstring; + } + + if !logical_line.is_comment_only { + state.is_not_first_logical_line = true; + + state.last_non_comment_line_end = logical_line.logical_line_end; + + if logical_line.indent_length == 0 { + state.previous_unindented_line_kind = Some(logical_line.kind); + } + } + if !logical_line.is_comment_only { prev_indent_length = Some(logical_line.indent_length); } @@ -644,235 +722,245 @@ impl BlankLinesChecker { #[allow(clippy::nonminimal_bool)] fn check_line( - &mut self, + &self, line: &LogicalLineInfo, + state: &BlankLinesState, prev_indent_length: Option, - locator: &Locator, - stylist: &Stylist, diagnostics: &mut Vec, ) { - self.class_status.update(line); - self.fn_status.update(line); - - // Don't expect blank lines before the first non comment line. - if self.is_not_first_logical_line { - if line.preceding_blank_lines == 0 - // Only applies to methods. - && matches!(line.kind, LogicalLineKind::Function | LogicalLineKind::Decorator) - // Allow groups of one-liners. - && !(matches!(self.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon)) - && matches!(self.class_status, Status::Inside(_)) - // The class/parent method's docstring can directly precede the def. - // Allow following a decorator (if there is an error it will be triggered on the first decorator). - && !matches!(self.follows, Follows::Docstring | Follows::Decorator) - // Do not trigger when the def follows an if/while/etc... - && prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length) - { - // E301 - let mut diagnostic = - Diagnostic::new(BlankLineBetweenMethods, line.first_token_range); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - stylist.line_ending().to_string(), - locator.line_start(self.last_non_comment_line_end), - ))); + if line.preceding_blank_lines == 0 + // Only applies to methods. + && matches!(line.kind, LogicalLineKind::Function | LogicalLineKind::Decorator) + // Allow groups of one-liners. + && !(matches!(state.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon)) + && matches!(state.class_status, Status::Inside(_)) + // The class/parent method's docstring can directly precede the def. + // Allow following a decorator (if there is an error it will be triggered on the first decorator). + && !matches!(state.follows, Follows::Docstring | Follows::Decorator) + // Do not trigger when the def follows an if/while/etc... + && prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length) + { + // E301 + let mut diagnostic = Diagnostic::new(BlankLineBetweenMethods, line.first_token_range); + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + self.stylist.line_ending().to_string(), + self.locator.line_start(state.last_non_comment_line_end), + ))); + + diagnostics.push(diagnostic); + } - diagnostics.push(diagnostic); + let expected_blank_lines_before_definition = if line.indent_length == 0 { + // Mimic the isort rules for the number of blank lines before classes and functions + if state.follows.is_any_import() { + // Fallback to the default if the value is too large for an u32 or if it is negative. + // A negative value means that isort should determine the blank lines automatically. + // `isort` defaults to 2 if before a class or function definition and 1 otherwise. + // Defaulting to 2 here is correct because the variable is only used when testing the + // blank lines before a class or function definition. + u32::try_from(self.lines_after_imports).unwrap_or(BLANK_LINES_TOP_LEVEL) + } else { + BLANK_LINES_TOP_LEVEL } + } else { + BLANK_LINES_NESTED_LEVEL + }; + + if line.preceding_blank_lines < expected_blank_lines_before_definition + // Allow following a decorator (if there is an error it will be triggered on the first decorator). + && !matches!(state.follows, Follows::Decorator) + // Allow groups of one-liners. + && !(matches!(state.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon)) + // Only trigger on non-indented classes and functions (for example functions within an if are ignored) + && line.indent_length == 0 + // Only apply to functions or classes. + && line.kind.is_class_function_or_decorator() + { + // E302 + let mut diagnostic = Diagnostic::new( + BlankLinesTopLevel { + actual_blank_lines: line.preceding_blank_lines.count(), + expected_blank_lines: expected_blank_lines_before_definition, + }, + line.first_token_range, + ); - if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL - // Allow following a decorator (if there is an error it will be triggered on the first decorator). - && !matches!(self.follows, Follows::Decorator) - // Allow groups of one-liners. - && !(matches!(self.follows, Follows::Def) && !matches!(line.last_token, TokenKind::Colon)) - // Only trigger on non-indented classes and functions (for example functions within an if are ignored) - && line.indent_length == 0 - // Only apply to functions or classes. - && line.kind.is_top_level() - { - // E302 - let mut diagnostic = Diagnostic::new( - BlankLinesTopLevel { - actual_blank_lines: line.preceding_blank_lines.count(), - }, - line.first_token_range, - ); - - if let Some(blank_lines_range) = line.blank_lines.range() { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize), - blank_lines_range, - ))); - } else { - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize), - locator.line_start(self.last_non_comment_line_end), - ))); - } - - diagnostics.push(diagnostic); + if let Some(blank_lines_range) = line.blank_lines.range() { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + self.stylist + .line_ending() + .repeat(expected_blank_lines_before_definition as usize), + blank_lines_range, + ))); + } else { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + self.stylist + .line_ending() + .repeat(expected_blank_lines_before_definition as usize), + self.locator.line_start(state.last_non_comment_line_end), + ))); } - let expected_blank_lines = if line.indent_length > 0 { - BLANK_LINES_METHOD_LEVEL - } else { - BLANK_LINES_TOP_LEVEL - }; + diagnostics.push(diagnostic); + } - if line.blank_lines > expected_blank_lines { - // E303 - let mut diagnostic = Diagnostic::new( - TooManyBlankLines { - actual_blank_lines: line.blank_lines.count(), - }, - line.first_token_range, - ); + let max_lines_level = if line.indent_length == 0 { + BLANK_LINES_TOP_LEVEL + } else { + BLANK_LINES_NESTED_LEVEL + }; + + // If between `import` and `from .. import ..` or the other way round, + // allow up to `lines_between_types` newlines for isort compatibility. + // We let `isort` remove extra blank lines when the imports belong + // to different sections. + let max_blank_lines = if matches!( + (line.kind, state.follows), + (LogicalLineKind::Import, Follows::FromImport) + | (LogicalLineKind::FromImport, Follows::Import) + ) { + max_lines_level.max(u32::try_from(self.lines_between_types).unwrap_or(u32::MAX)) + } else { + expected_blank_lines_before_definition + }; + + if line.blank_lines > max_blank_lines { + // E303 + let mut diagnostic = Diagnostic::new( + TooManyBlankLines { + actual_blank_lines: line.blank_lines.count(), + }, + line.first_token_range, + ); - if let Some(blank_lines_range) = line.blank_lines.range() { + if let Some(blank_lines_range) = line.blank_lines.range() { + if max_blank_lines == 0 { + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(blank_lines_range))); + } else { diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - stylist.line_ending().repeat(expected_blank_lines as usize), + self.stylist.line_ending().repeat(max_blank_lines as usize), blank_lines_range, ))); } - - diagnostics.push(diagnostic); } - if matches!(self.follows, Follows::Decorator) - && !line.is_comment_only - && line.preceding_blank_lines > 0 - { - // E304 - let mut diagnostic = Diagnostic::new( - BlankLineAfterDecorator { - actual_blank_lines: line.preceding_blank_lines.count(), - }, - line.first_token_range, - ); - - // Get all the lines between the last decorator line (included) and the current line (included). - // Then remove all blank lines. - let trivia_range = TextRange::new( - self.last_non_comment_line_end, - locator.line_start(line.first_token_range.start()), - ); - let trivia_text = locator.slice(trivia_range); - let mut trivia_without_blank_lines = trivia_text - .universal_newlines() - .filter_map(|line| { - (!line.trim_whitespace().is_empty()).then_some(line.as_str()) - }) - .join(&stylist.line_ending()); - - let fix = if trivia_without_blank_lines.is_empty() { - Fix::safe_edit(Edit::range_deletion(trivia_range)) - } else { - trivia_without_blank_lines.push_str(&stylist.line_ending()); - Fix::safe_edit(Edit::range_replacement( - trivia_without_blank_lines, - trivia_range, - )) - }; + diagnostics.push(diagnostic); + } - diagnostic.set_fix(fix); + if matches!(state.follows, Follows::Decorator) + && !line.is_comment_only + && line.preceding_blank_lines > 0 + { + // E304 + let mut diagnostic = Diagnostic::new( + BlankLineAfterDecorator { + actual_blank_lines: line.preceding_blank_lines.count(), + }, + line.first_token_range, + ); - diagnostics.push(diagnostic); - } + // Get all the lines between the last decorator line (included) and the current line (included). + // Then remove all blank lines. + let trivia_range = TextRange::new( + state.last_non_comment_line_end, + self.locator.line_start(line.first_token_range.start()), + ); + let trivia_text = self.locator.slice(trivia_range); + let mut trivia_without_blank_lines = trivia_text + .universal_newlines() + .filter_map(|line| (!line.trim_whitespace().is_empty()).then_some(line.as_str())) + .join(&self.stylist.line_ending()); + + let fix = if trivia_without_blank_lines.is_empty() { + Fix::safe_edit(Edit::range_deletion(trivia_range)) + } else { + trivia_without_blank_lines.push_str(&self.stylist.line_ending()); + Fix::safe_edit(Edit::range_replacement( + trivia_without_blank_lines, + trivia_range, + )) + }; - if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL - && self - .previous_unindented_line_kind - .is_some_and(LogicalLineKind::is_top_level) - && line.indent_length == 0 - && !line.is_comment_only - && !line.kind.is_top_level() - { - // E305 - let mut diagnostic = Diagnostic::new( - BlankLinesAfterFunctionOrClass { - actual_blank_lines: line.preceding_blank_lines.count(), - }, - line.first_token_range, - ); - - if let Some(blank_lines_range) = line.blank_lines.range() { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize), - blank_lines_range, - ))); - } else { - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - stylist.line_ending().repeat(BLANK_LINES_TOP_LEVEL as usize), - locator.line_start(line.first_token_range.start()), - ))); - } + diagnostic.set_fix(fix); - diagnostics.push(diagnostic); - } + diagnostics.push(diagnostic); + } - if line.preceding_blank_lines == 0 - // Only apply to nested functions. - && matches!(self.fn_status, Status::Inside(_)) - && line.kind.is_top_level() - // Allow following a decorator (if there is an error it will be triggered on the first decorator). - && !matches!(self.follows, Follows::Decorator) - // The class's docstring can directly precede the first function. - && !matches!(self.follows, Follows::Docstring) - // Do not trigger when the def/class follows an "indenting token" (if/while/etc...). - && prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length) - // Allow groups of one-liners. - && !(matches!(self.follows, Follows::Def) && line.last_token != TokenKind::Colon) - { - // E306 - let mut diagnostic = - Diagnostic::new(BlankLinesBeforeNestedDefinition, line.first_token_range); + if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL + && state + .previous_unindented_line_kind + .is_some_and(LogicalLineKind::is_class_function_or_decorator) + && line.indent_length == 0 + && !line.is_comment_only + && !line.kind.is_class_function_or_decorator() + { + // E305 + let mut diagnostic = Diagnostic::new( + BlankLinesAfterFunctionOrClass { + actual_blank_lines: line.preceding_blank_lines.count(), + }, + line.first_token_range, + ); + if let Some(blank_lines_range) = line.blank_lines.range() { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + self.stylist + .line_ending() + .repeat(BLANK_LINES_TOP_LEVEL as usize), + blank_lines_range, + ))); + } else { diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - stylist.line_ending().to_string(), - locator.line_start(line.first_token_range.start()), + self.stylist + .line_ending() + .repeat(BLANK_LINES_TOP_LEVEL as usize), + self.locator.line_start(line.first_token_range.start()), ))); - - diagnostics.push(diagnostic); - } - } - - match line.kind { - LogicalLineKind::Class => { - if matches!(self.class_status, Status::Outside) { - self.class_status = Status::Inside(line.indent_length); - } - self.follows = Follows::Other; - } - LogicalLineKind::Decorator => { - self.follows = Follows::Decorator; - } - LogicalLineKind::Function => { - if matches!(self.fn_status, Status::Outside) { - self.fn_status = Status::Inside(line.indent_length); - } - self.follows = Follows::Def; - } - LogicalLineKind::Comment => {} - LogicalLineKind::Other => { - self.follows = Follows::Other; } - } - if line.is_docstring { - self.follows = Follows::Docstring; + diagnostics.push(diagnostic); } - if !line.is_comment_only { - self.is_not_first_logical_line = true; - - self.last_non_comment_line_end = line.logical_line_end; - - if line.indent_length == 0 { - self.previous_unindented_line_kind = Some(line.kind); - } + if line.preceding_blank_lines == 0 + // Only apply to nested functions. + && matches!(state.fn_status, Status::Inside(_)) + && line.kind.is_class_function_or_decorator() + // Allow following a decorator (if there is an error it will be triggered on the first decorator). + && !matches!(state.follows, Follows::Decorator) + // The class's docstring can directly precede the first function. + && !matches!(state.follows, Follows::Docstring) + // Do not trigger when the def/class follows an "indenting token" (if/while/etc...). + && prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length) + // Allow groups of one-liners. + && !(matches!(state.follows, Follows::Def) && line.last_token != TokenKind::Colon) + { + // E306 + let mut diagnostic = + Diagnostic::new(BlankLinesBeforeNestedDefinition, line.first_token_range); + + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + self.stylist.line_ending().to_string(), + self.locator.line_start(line.first_token_range.start()), + ))); + + diagnostics.push(diagnostic); } } } +#[derive(Clone, Debug, Default)] +struct BlankLinesState { + follows: Follows, + fn_status: Status, + class_status: Status, + /// First line that is not a comment. + is_not_first_logical_line: bool, + /// Used for the fix in case a comment separates two non-comment logical lines to make the comment "stick" + /// to the second line instead of the first. + last_non_comment_line_end: TextSize, + previous_unindented_line_kind: Option, +} + #[derive(Copy, Clone, Debug)] enum LogicalLineKind { /// The clause header of a class definition @@ -883,12 +971,16 @@ enum LogicalLineKind { Function, /// A comment only line Comment, + /// An import statement + Import, + /// A from.. import statement + FromImport, /// Any other statement or clause header Other, } impl LogicalLineKind { - fn is_top_level(self) -> bool { + fn is_class_function_or_decorator(self) -> bool { matches!( self, LogicalLineKind::Class | LogicalLineKind::Function | LogicalLineKind::Decorator diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap index 1c854a6ad3ad0..4f921f2583cdf 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap @@ -245,6 +245,4 @@ E30.py:702:5: E303 [*] Too many blank lines (2) 701 |- 702 701 | pass 703 702 | # end -704 703 | - - +704 703 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap new file mode 100644 index 0000000000000..7b01f96447d7e --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(-1)-between(0).snap @@ -0,0 +1,140 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +1 1 | import json +2 |- +3 |- +4 |- +5 2 | from typing import Any, Sequence +6 3 | +7 4 | + +E30_isort.py:23:1: E302 [*] Expected 2 blank lines, found 1 + | +21 | abcd.foo() +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | ^^^ E302 +24 | +25 | if TYPE_CHECKING: + | + = help: Add missing blank line(s) + +ℹ Safe fix +20 20 | +21 21 | abcd.foo() +22 22 | + 23 |+ +23 24 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... +24 25 | +25 26 | if TYPE_CHECKING: + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:35:1: E302 [*] Expected 2 blank lines, found 1 + | +33 | abcd.foo() +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +36 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +32 32 | +33 33 | abcd.foo() +34 34 | + 35 |+ +35 36 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: +36 37 | ... +37 38 | + +E30_isort.py:41:1: E302 [*] Expected 2 blank lines, found 1 + | +39 | from typing_extensions import TypeAlias +40 | +41 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +42 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +38 38 | if TYPE_CHECKING: +39 39 | from typing_extensions import TypeAlias +40 40 | + 41 |+ +41 42 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: +42 43 | ... +43 44 | + +E30_isort.py:60:1: I001 [*] Import block is un-sorted or un-formatted + | +60 | / from typing import Any, Sequence +61 | | +62 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +59 59 | +60 60 | from typing import Any, Sequence +61 61 | + 62 |+ +62 63 | class MissingCommand(TypeError): ... # noqa: N818 + +E30_isort.py:62:1: E302 [*] Expected 2 blank lines, found 1 + | +60 | from typing import Any, Sequence +61 | +62 | class MissingCommand(TypeError): ... # noqa: N818 + | ^^^^^ E302 + | + = help: Add missing blank line(s) + +ℹ Safe fix +59 59 | +60 60 | from typing import Any, Sequence +61 61 | + 62 |+ +62 63 | class MissingCommand(TypeError): ... # noqa: N818 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap new file mode 100644 index 0000000000000..92e7ce4e2e43f --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(0)-between(0).snap @@ -0,0 +1,127 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +1 1 | import json +2 |- +3 |- +4 |- +5 2 | from typing import Any, Sequence +6 |- +7 |- +8 3 | class MissingCommand(TypeError): ... # noqa: N818 +9 4 | +10 5 | + +E30_isort.py:23:1: E302 [*] Expected 2 blank lines, found 1 + | +21 | abcd.foo() +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | ^^^ E302 +24 | +25 | if TYPE_CHECKING: + | + = help: Add missing blank line(s) + +ℹ Safe fix +20 20 | +21 21 | abcd.foo() +22 22 | + 23 |+ +23 24 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... +24 25 | +25 26 | if TYPE_CHECKING: + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:35:1: E302 [*] Expected 2 blank lines, found 1 + | +33 | abcd.foo() +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +36 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +32 32 | +33 33 | abcd.foo() +34 34 | + 35 |+ +35 36 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: +36 37 | ... +37 38 | + +E30_isort.py:41:1: E302 [*] Expected 2 blank lines, found 1 + | +39 | from typing_extensions import TypeAlias +40 | +41 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +42 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +38 38 | if TYPE_CHECKING: +39 39 | from typing_extensions import TypeAlias +40 40 | + 41 |+ +41 42 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: +42 43 | ... +43 44 | + +E30_isort.py:60:1: I001 [*] Import block is un-sorted or un-formatted + | +60 | / from typing import Any, Sequence +61 | | +62 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +58 58 | +59 59 | +60 60 | from typing import Any, Sequence +61 |- +62 61 | class MissingCommand(TypeError): ... # noqa: N818 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap new file mode 100644 index 0000000000000..2e8fed4bd6c27 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(1)-between(1).snap @@ -0,0 +1,110 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +1 1 | import json +2 2 | +3 |- +4 |- +5 3 | from typing import Any, Sequence +6 |- +7 4 | +8 5 | class MissingCommand(TypeError): ... # noqa: N818 +9 6 | + +E30_isort.py:23:1: E302 [*] Expected 2 blank lines, found 1 + | +21 | abcd.foo() +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | ^^^ E302 +24 | +25 | if TYPE_CHECKING: + | + = help: Add missing blank line(s) + +ℹ Safe fix +20 20 | +21 21 | abcd.foo() +22 22 | + 23 |+ +23 24 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... +24 25 | +25 26 | if TYPE_CHECKING: + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:35:1: E302 [*] Expected 2 blank lines, found 1 + | +33 | abcd.foo() +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +36 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +32 32 | +33 33 | abcd.foo() +34 34 | + 35 |+ +35 36 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: +36 37 | ... +37 38 | + +E30_isort.py:41:1: E302 [*] Expected 2 blank lines, found 1 + | +39 | from typing_extensions import TypeAlias +40 | +41 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +42 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +38 38 | if TYPE_CHECKING: +39 39 | from typing_extensions import TypeAlias +40 40 | + 41 |+ +41 42 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: +42 43 | ... +43 44 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap new file mode 100644 index 0000000000000..c24ef9e346b1d --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_top_level_isort_compatibility-lines-after(4)-between(4).snap @@ -0,0 +1,166 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +2 2 | +3 3 | +4 4 | + 5 |+ +5 6 | from typing import Any, Sequence +6 7 | +7 8 | + 9 |+ + 10 |+ +8 11 | class MissingCommand(TypeError): ... # noqa: N818 +9 12 | +10 13 | + +E30_isort.py:8:1: E302 [*] Expected 4 blank lines, found 2 + | +8 | class MissingCommand(TypeError): ... # noqa: N818 + | ^^^^^ E302 + | + = help: Add missing blank line(s) + +ℹ Safe fix +5 5 | from typing import Any, Sequence +6 6 | +7 7 | + 8 |+ + 9 |+ +8 10 | class MissingCommand(TypeError): ... # noqa: N818 +9 11 | +10 12 | + +E30_isort.py:23:1: E302 [*] Expected 2 blank lines, found 1 + | +21 | abcd.foo() +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | ^^^ E302 +24 | +25 | if TYPE_CHECKING: + | + = help: Add missing blank line(s) + +ℹ Safe fix +20 20 | +21 21 | abcd.foo() +22 22 | + 23 |+ +23 24 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... +24 25 | +25 26 | if TYPE_CHECKING: + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:35:1: E302 [*] Expected 2 blank lines, found 1 + | +33 | abcd.foo() +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +36 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +32 32 | +33 33 | abcd.foo() +34 34 | + 35 |+ +35 36 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: +36 37 | ... +37 38 | + +E30_isort.py:41:1: E302 [*] Expected 2 blank lines, found 1 + | +39 | from typing_extensions import TypeAlias +40 | +41 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: + | ^^^ E302 +42 | ... + | + = help: Add missing blank line(s) + +ℹ Safe fix +38 38 | if TYPE_CHECKING: +39 39 | from typing_extensions import TypeAlias +40 40 | + 41 |+ +41 42 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any: +42 43 | ... +43 44 | + +E30_isort.py:60:1: I001 [*] Import block is un-sorted or un-formatted + | +60 | / from typing import Any, Sequence +61 | | +62 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +59 59 | +60 60 | from typing import Any, Sequence +61 61 | + 62 |+ + 63 |+ + 64 |+ +62 65 | class MissingCommand(TypeError): ... # noqa: N818 + +E30_isort.py:62:1: E302 [*] Expected 4 blank lines, found 1 + | +60 | from typing import Any, Sequence +61 | +62 | class MissingCommand(TypeError): ... # noqa: N818 + | ^^^^^ E302 + | + = help: Add missing blank line(s) + +ℹ Safe fix +59 59 | +60 60 | from typing import Any, Sequence +61 61 | + 62 |+ + 63 |+ + 64 |+ +62 65 | class MissingCommand(TypeError): ... # noqa: N818 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap new file mode 100644 index 0000000000000..8e0ac4d6f2f6e --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(-1)-between(0).snap @@ -0,0 +1,135 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +1 1 | import json +2 |- +3 |- +4 |- +5 2 | from typing import Any, Sequence +6 3 | +7 4 | + +E30_isort.py:5:1: E303 [*] Too many blank lines (3) + | +5 | from typing import Any, Sequence + | ^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +1 1 | import json +2 2 | +3 3 | +4 |- +5 4 | from typing import Any, Sequence +6 5 | +7 6 | + +E30_isort.py:21:5: E303 [*] Too many blank lines (2) + | +21 | abcd.foo() + | ^^^^ E303 +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +17 17 | if __name__ == "__main__": +18 18 | import abcd +19 19 | +20 |- +21 20 | abcd.foo() +22 21 | +23 22 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:30:5: E303 [*] Too many blank lines (3) + | +30 | from typing_extensions import TypeAlias + | ^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:33:5: E303 [*] Too many blank lines (2) + | +33 | abcd.foo() + | ^^^^ E303 +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +29 29 | +30 30 | from typing_extensions import TypeAlias +31 31 | +32 |- +33 32 | abcd.foo() +34 33 | +35 34 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + +E30_isort.py:60:1: I001 [*] Import block is un-sorted or un-formatted + | +60 | / from typing import Any, Sequence +61 | | +62 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +59 59 | +60 60 | from typing import Any, Sequence +61 61 | + 62 |+ +62 63 | class MissingCommand(TypeError): ... # noqa: N818 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap new file mode 100644 index 0000000000000..092f7ecbde343 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(0)-between(0).snap @@ -0,0 +1,171 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +1 1 | import json +2 |- +3 |- +4 |- +5 2 | from typing import Any, Sequence +6 |- +7 |- +8 3 | class MissingCommand(TypeError): ... # noqa: N818 +9 4 | +10 5 | + +E30_isort.py:5:1: E303 [*] Too many blank lines (3) + | +5 | from typing import Any, Sequence + | ^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +1 1 | import json +2 2 | +3 3 | +4 |- +5 4 | from typing import Any, Sequence +6 5 | +7 6 | + +E30_isort.py:8:1: E303 [*] Too many blank lines (2) + | +8 | class MissingCommand(TypeError): ... # noqa: N818 + | ^^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +3 3 | +4 4 | +5 5 | from typing import Any, Sequence +6 |- +7 |- +8 6 | class MissingCommand(TypeError): ... # noqa: N818 +9 7 | +10 8 | + +E30_isort.py:21:5: E303 [*] Too many blank lines (2) + | +21 | abcd.foo() + | ^^^^ E303 +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +17 17 | if __name__ == "__main__": +18 18 | import abcd +19 19 | +20 |- +21 20 | abcd.foo() +22 21 | +23 22 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:30:5: E303 [*] Too many blank lines (3) + | +30 | from typing_extensions import TypeAlias + | ^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:33:5: E303 [*] Too many blank lines (2) + | +33 | abcd.foo() + | ^^^^ E303 +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +29 29 | +30 30 | from typing_extensions import TypeAlias +31 31 | +32 |- +33 32 | abcd.foo() +34 33 | +35 34 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + +E30_isort.py:60:1: I001 [*] Import block is un-sorted or un-formatted + | +60 | / from typing import Any, Sequence +61 | | +62 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +58 58 | +59 59 | +60 60 | from typing import Any, Sequence +61 |- +62 61 | class MissingCommand(TypeError): ... # noqa: N818 + +E30_isort.py:62:1: E303 [*] Too many blank lines (1) + | +60 | from typing import Any, Sequence +61 | +62 | class MissingCommand(TypeError): ... # noqa: N818 + | ^^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +58 58 | +59 59 | +60 60 | from typing import Any, Sequence +61 |- +62 61 | class MissingCommand(TypeError): ... # noqa: N818 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap new file mode 100644 index 0000000000000..3ba9a6f5e8c21 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(1)-between(1).snap @@ -0,0 +1,137 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +1 1 | import json +2 2 | +3 |- +4 |- +5 3 | from typing import Any, Sequence +6 |- +7 4 | +8 5 | class MissingCommand(TypeError): ... # noqa: N818 +9 6 | + +E30_isort.py:5:1: E303 [*] Too many blank lines (3) + | +5 | from typing import Any, Sequence + | ^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +1 1 | import json +2 2 | +3 3 | +4 |- +5 4 | from typing import Any, Sequence +6 5 | +7 6 | + +E30_isort.py:8:1: E303 [*] Too many blank lines (2) + | +8 | class MissingCommand(TypeError): ... # noqa: N818 + | ^^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +4 4 | +5 5 | from typing import Any, Sequence +6 6 | +7 |- +8 7 | class MissingCommand(TypeError): ... # noqa: N818 +9 8 | +10 9 | + +E30_isort.py:21:5: E303 [*] Too many blank lines (2) + | +21 | abcd.foo() + | ^^^^ E303 +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +17 17 | if __name__ == "__main__": +18 18 | import abcd +19 19 | +20 |- +21 20 | abcd.foo() +22 21 | +23 22 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:30:5: E303 [*] Too many blank lines (3) + | +30 | from typing_extensions import TypeAlias + | ^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:33:5: E303 [*] Too many blank lines (2) + | +33 | abcd.foo() + | ^^^^ E303 +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +29 29 | +30 30 | from typing_extensions import TypeAlias +31 31 | +32 |- +33 32 | abcd.foo() +34 33 | +35 34 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap new file mode 100644 index 0000000000000..b4ebcc11a63ce --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__too_many_blank_lines_isort_compatibility-lines-after(4)-between(4).snap @@ -0,0 +1,109 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30_isort.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import json +2 | | +3 | | +4 | | +5 | | from typing import Any, Sequence +6 | | +7 | | +8 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +2 2 | +3 3 | +4 4 | + 5 |+ +5 6 | from typing import Any, Sequence +6 7 | +7 8 | + 9 |+ + 10 |+ +8 11 | class MissingCommand(TypeError): ... # noqa: N818 +9 12 | +10 13 | + +E30_isort.py:21:5: E303 [*] Too many blank lines (2) + | +21 | abcd.foo() + | ^^^^ E303 +22 | +23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +17 17 | if __name__ == "__main__": +18 18 | import abcd +19 19 | +20 |- +21 20 | abcd.foo() +22 21 | +23 22 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ... + +E30_isort.py:26:1: I001 [*] Import block is un-sorted or un-formatted + | +25 | if TYPE_CHECKING: +26 | / import os +27 | | +28 | | +29 | | +30 | | from typing_extensions import TypeAlias +31 | | + | |_^ I001 +32 | +33 | abcd.foo() + | + = help: Organize imports + +ℹ Safe fix +25 25 | if TYPE_CHECKING: +26 26 | import os +27 27 | +28 |- +29 |- +30 28 | from typing_extensions import TypeAlias +31 29 | +32 30 | + +E30_isort.py:33:5: E303 [*] Too many blank lines (2) + | +33 | abcd.foo() + | ^^^^ E303 +34 | +35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +29 29 | +30 30 | from typing_extensions import TypeAlias +31 31 | +32 |- +33 32 | abcd.foo() +34 33 | +35 34 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: + +E30_isort.py:60:1: I001 [*] Import block is un-sorted or un-formatted + | +60 | / from typing import Any, Sequence +61 | | +62 | | class MissingCommand(TypeError): ... # noqa: N818 + | |_^ I001 + | + = help: Organize imports + +ℹ Safe fix +59 59 | +60 60 | from typing import Any, Sequence +61 61 | + 62 |+ + 63 |+ + 64 |+ +62 65 | class MissingCommand(TypeError): ... # noqa: N818 diff --git a/crates/ruff_python_codegen/src/stylist.rs b/crates/ruff_python_codegen/src/stylist.rs index e54ef0e1ccd63..acd241d027b58 100644 --- a/crates/ruff_python_codegen/src/stylist.rs +++ b/crates/ruff_python_codegen/src/stylist.rs @@ -12,6 +12,7 @@ use ruff_source_file::{find_newline, LineEnding}; use ruff_python_ast::str::leading_quote; use ruff_source_file::Locator; +#[derive(Debug, Clone)] pub struct Stylist<'a> { locator: &'a Locator<'a>, indentation: Indentation, @@ -145,7 +146,7 @@ impl fmt::Display for Quote { } /// The indentation style used in Python source code. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Indentation(String); impl Indentation { diff --git a/crates/ruff_source_file/src/locator.rs b/crates/ruff_source_file/src/locator.rs index a642ce4afe957..792f31e247f44 100644 --- a/crates/ruff_source_file/src/locator.rs +++ b/crates/ruff_source_file/src/locator.rs @@ -9,6 +9,7 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::newlines::find_newline; use crate::{LineIndex, OneIndexed, SourceCode, SourceLocation}; +#[derive(Debug)] pub struct Locator<'a> { contents: &'a str, index: OnceCell,