From 30495216bcab1e307f2487d6f53725db58d5be18 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 23 Feb 2024 16:04:46 +0100 Subject: [PATCH] Typing stub file support for blank line rules --- .../test/fixtures/pycodestyle/E30.pyi | 50 ++++++++++++ crates/ruff_linter/src/checkers/tokens.rs | 3 +- .../ruff_linter/src/rules/pycodestyle/mod.rs | 16 ++++ .../rules/pycodestyle/rules/blank_lines.rs | 77 ++++++++++++++----- ...__tests__blank_lines_E301_typing_stub.snap | 4 + ...__tests__blank_lines_E302_typing_stub.snap | 4 + ...__tests__blank_lines_E303_typing_stub.snap | 73 ++++++++++++++++++ ...__tests__blank_lines_E304_typing_stub.snap | 20 +++++ ...__tests__blank_lines_E305_typing_stub.snap | 4 + ...__tests__blank_lines_E306_typing_stub.snap | 4 + 10 files changed, 236 insertions(+), 19 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.pyi create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E301_typing_stub.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E302_typing_stub.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E303_typing_stub.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E305_typing_stub.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E306_typing_stub.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.pyi b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.pyi new file mode 100644 index 00000000000000..96323e1e8a0929 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.pyi @@ -0,0 +1,50 @@ +import json + +from typing import Any, Sequence + +class MissingCommand(TypeError): ... +class AnoherClass: ... + +def a(): ... + +@overload +def a(arg: int): ... + +@overload +def a(arg: int, name: str): ... + + +def grouped1(): ... +def grouped2(): ... +def grouped3( ): ... + + +class BackendProxy: + backend_module: str + backend_object: str | None + backend: Any + + def grouped1(): ... + def grouped2(): ... + def grouped3( ): ... + @decorated + + def with_blank_line(): ... + + + def ungrouped(): ... +a = "test" + +def function_def(): + pass +b = "test" + + +def outer(): + def inner(): + pass + def inner2(): + pass + +class Foo: ... +class Bar: ... diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index dea5c8d8a9e4d4..762f4cc463cc10 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -41,7 +41,8 @@ pub(crate) fn check_tokens( Rule::BlankLinesAfterFunctionOrClass, Rule::BlankLinesBeforeNestedDefinition, ]) { - BlankLinesChecker::new(locator, stylist, settings).check_lines(tokens, &mut diagnostics); + BlankLinesChecker::new(locator, stylist, settings, source_type) + .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 cf2e002f492d81..b3f7dd99ef1735 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -222,6 +222,22 @@ mod tests { Ok(()) } + #[test_case(Rule::BlankLineBetweenMethods)] + #[test_case(Rule::BlankLinesTopLevel)] + #[test_case(Rule::TooManyBlankLines)] + #[test_case(Rule::BlankLineAfterDecorator)] + #[test_case(Rule::BlankLinesAfterFunctionOrClass)] + #[test_case(Rule::BlankLinesBeforeNestedDefinition)] + fn blank_lines_typing_stub(rule_code: Rule) -> Result<()> { + let snapshot = format!("blank_lines_{}_typing_stub", rule_code.noqa_code()); + let diagnostics = test_path( + Path::new("pycodestyle").join("E30.pyi"), + &settings::LinterSettings::for_rule(rule_code), + )?; + 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 50dea8ee821d9b..71908aa8095c12 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -8,6 +8,7 @@ use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Edit; use ruff_diagnostics::Fix; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::PySourceType; use ruff_python_codegen::Stylist; use ruff_python_parser::lexer::LexResult; use ruff_python_parser::lexer::LexicalError; @@ -51,9 +52,14 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1; /// pass /// ``` /// +/// ## Typing stub files (`.pyi`) +/// The typing style guide recommends to not use blank lines between methods except to group +/// them. That's why this rule is not enabled in typing stub files. +/// /// ## References /// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html) +/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] pub struct BlankLineBetweenMethods; @@ -96,9 +102,14 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods { /// pass /// ``` /// +/// ## Typing stub files (`.pyi`) +/// The typing style guide recommends to not use blank lines between classes and functions except to group +/// them. That's why this rule is not enabled in typing stub files. +/// /// ## References /// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html) +/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] pub struct BlankLinesTopLevel { actual_blank_lines: u32, @@ -150,6 +161,9 @@ impl AlwaysFixableViolation for BlankLinesTopLevel { /// pass /// ``` /// +/// ## Typing stub files (`.pyi`) +/// The rule allows at most one blank line in typing stub files in accordance to the typing style guide recommendation. +/// /// 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 an `from..import` statement or vice versa. @@ -157,6 +171,7 @@ impl AlwaysFixableViolation for BlankLinesTopLevel { /// ## References /// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html) +/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] pub struct TooManyBlankLines { actual_blank_lines: u32, @@ -246,9 +261,14 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator { /// user = User() /// ``` /// +/// ## Typing stub files (`.pyi`) +/// The typing style guide recommends to not use blank lines between statements except to group +/// them. That's why this rule is not enabled in typing stub files. +/// /// ## References /// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html) +/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] pub struct BlankLinesAfterFunctionOrClass { actual_blank_lines: u32, @@ -295,9 +315,14 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass { /// pass /// ``` /// +/// ## Typing stub files (`.pyi`) +/// The typing style guide recommends to not use blank lines between classes and functions except to group +/// them. That's why this rule is not enabled in typing stub files. +/// /// ## References /// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) /// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html) +/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) #[violation] pub struct BlankLinesBeforeNestedDefinition; @@ -628,6 +653,7 @@ pub(crate) struct BlankLinesChecker<'a> { indent_width: IndentWidth, lines_after_imports: isize, lines_between_types: usize, + source_type: PySourceType, } impl<'a> BlankLinesChecker<'a> { @@ -635,6 +661,7 @@ impl<'a> BlankLinesChecker<'a> { locator: &'a Locator<'a>, stylist: &'a Stylist<'a>, settings: &crate::settings::LinterSettings, + source_type: PySourceType, ) -> BlankLinesChecker<'a> { BlankLinesChecker { stylist, @@ -642,6 +669,7 @@ impl<'a> BlankLinesChecker<'a> { indent_width: settings.tab_size, lines_after_imports: settings.isort.lines_after_imports, lines_between_types: settings.isort.lines_between_types, + source_type, } } @@ -739,6 +767,8 @@ impl<'a> BlankLinesChecker<'a> { && !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) + // Blank lines in stub files are only used for grouping. Don't enforce blank lines. + && !self.source_type.is_stub() { // E301 let mut diagnostic = Diagnostic::new(BlankLineBetweenMethods, line.first_token_range); @@ -774,6 +804,8 @@ impl<'a> BlankLinesChecker<'a> { && line.indent_length == 0 // Only apply to functions or classes. && line.kind.is_class_function_or_decorator() + // Blank lines in stub files are used to group definitions. Don't enforce blank lines. + && !self.source_type.is_stub() { // E302 let mut diagnostic = Diagnostic::new( @@ -803,28 +835,33 @@ impl<'a> BlankLinesChecker<'a> { diagnostics.push(diagnostic); } - let max_lines_level = if line.indent_length == 0 { - BLANK_LINES_TOP_LEVEL + // Blank lines in stub files are used to group definitions. Don't enforce blank lines. + let max_blank_lines = if self.source_type.is_stub() { + 1 } else { - BLANK_LINES_NESTED_LEVEL - }; + 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) - ) { - if self.lines_between_types == 0 { - max_lines_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. + if matches!( + (line.kind, state.follows), + (LogicalLineKind::Import, Follows::FromImport) + | (LogicalLineKind::FromImport, Follows::Import) + ) { + if self.lines_between_types == 0 { + max_lines_level + } else { + u32::try_from(self.lines_between_types).unwrap_or(u32::MAX) + } } else { - u32::try_from(self.lines_between_types).unwrap_or(u32::MAX) + expected_blank_lines_before_definition } - } else { - expected_blank_lines_before_definition }; if line.blank_lines > max_blank_lines { @@ -896,6 +933,8 @@ impl<'a> BlankLinesChecker<'a> { && line.indent_length == 0 && !line.is_comment_only && !line.kind.is_class_function_or_decorator() + // Blank lines in stub files are used for grouping, don't enforce blank lines. + && !self.source_type.is_stub() { // E305 let mut diagnostic = Diagnostic::new( @@ -936,6 +975,8 @@ impl<'a> BlankLinesChecker<'a> { && 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) + // Blank lines in stub files are only used for grouping. Don't enforce blank lines. + && !self.source_type.is_stub() { // E306 let mut diagnostic = diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E301_typing_stub.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E301_typing_stub.snap new file mode 100644 index 00000000000000..6dcc4546f11f9e --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E301_typing_stub.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E302_typing_stub.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E302_typing_stub.snap new file mode 100644 index 00000000000000..6dcc4546f11f9e --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E302_typing_stub.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E303_typing_stub.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E303_typing_stub.snap new file mode 100644 index 00000000000000..520a83ea86ff20 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E303_typing_stub.snap @@ -0,0 +1,73 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.pyi:17:1: E303 [*] Too many blank lines (2) + | +17 | def grouped1(): ... + | ^^^ E303 +18 | def grouped2(): ... +19 | def grouped3( ): ... + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +13 13 | @overload +14 14 | def a(arg: int, name: str): ... +15 15 | +16 |- +17 16 | def grouped1(): ... +18 17 | def grouped2(): ... +19 18 | def grouped3( ): ... + +E30.pyi:22:1: E303 [*] Too many blank lines (2) + | +22 | class BackendProxy: + | ^^^^^ E303 +23 | backend_module: str +24 | backend_object: str | None + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +18 18 | def grouped2(): ... +19 19 | def grouped3( ): ... +20 20 | +21 |- +22 21 | class BackendProxy: +23 22 | backend_module: str +24 23 | backend_object: str | None + +E30.pyi:35:5: E303 [*] Too many blank lines (2) + | +35 | def ungrouped(): ... + | ^^^ E303 +36 | a = "test" + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +31 31 | +32 32 | def with_blank_line(): ... +33 33 | +34 |- +35 34 | def ungrouped(): ... +36 35 | a = "test" +37 36 | + +E30.pyi:43:1: E303 [*] Too many blank lines (2) + | +43 | def outer(): + | ^^^ E303 +44 | def inner(): +45 | pass + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +39 39 | pass +40 40 | b = "test" +41 41 | +42 |- +43 42 | def outer(): +44 43 | def inner(): +45 44 | pass diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap new file mode 100644 index 00000000000000..565eeed36e6a6c --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E304_typing_stub.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.pyi:32:5: E304 [*] Blank lines found after function decorator (1) + | +30 | @decorated +31 | +32 | def with_blank_line(): ... + | ^^^ E304 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +28 28 | def grouped2(): ... +29 29 | def grouped3( ): ... +30 30 | @decorated +31 |- +32 31 | def with_blank_line(): ... +33 32 | +34 33 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E305_typing_stub.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E305_typing_stub.snap new file mode 100644 index 00000000000000..6dcc4546f11f9e --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E305_typing_stub.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E306_typing_stub.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E306_typing_stub.snap new file mode 100644 index 00000000000000..6dcc4546f11f9e --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__blank_lines_E306_typing_stub.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +