Skip to content

Commit

Permalink
Typing stub file support for blank line rules
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Mar 1, 2024
1 parent 066cc98 commit 3049521
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 19 deletions.
50 changes: 50 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
3 changes: 2 additions & 1 deletion crates/ruff_linter/src/checkers/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions crates/ruff_linter/src/rules/pycodestyle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
77 changes: 59 additions & 18 deletions crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -150,13 +161,17 @@ 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.
///
/// ## 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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -628,20 +653,23 @@ pub(crate) struct BlankLinesChecker<'a> {
indent_width: IndentWidth,
lines_after_imports: isize,
lines_between_types: usize,
source_type: PySourceType,
}

impl<'a> BlankLinesChecker<'a> {
pub(crate) fn new(
locator: &'a Locator<'a>,
stylist: &'a Stylist<'a>,
settings: &crate::settings::LinterSettings,
source_type: PySourceType,
) -> 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,
source_type,
}
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

0 comments on commit 3049521

Please sign in to comment.