diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.py new file mode 100644 index 00000000000000..37c2e6d803ce77 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.py @@ -0,0 +1,816 @@ +"""Fixtures for the errors E301, E302, E303, E304, E305 and E306. +Since these errors are about new lines, each test starts with either "No error" or "# E30X". +Each test's end is signaled by a "# end" line. +There should be no E30X error outside of a test's bound. +""" + + +# No error +class Class: + pass +# end + + +# No error +class Class: + """Docstring""" + def __init__(self) -> None: + pass +# end + + +# No error +def func(): + pass +# end + + +# No error +# comment +class Class: + pass +# end + + +# No error +# comment +def func(): + pass +# end + + +# no error +def foo(): + pass + + +def bar(): + pass + + +class Foo(object): + pass + + +class Bar(object): + pass +# end + + +# No error +class Class(object): + + def func1(): + pass + + def func2(): + pass +# end + + +# No error +class Class(object): + + def func1(): + pass + +# comment + def func2(): + pass +# end + + +# No error +class Class: + + def func1(): + pass + + # comment + def func2(): + pass + + # This is a + # ... multi-line comment + + def func3(): + pass + + +# This is a +# ... multi-line comment + +@decorator +class Class: + + def func1(): + pass + + # comment + + def func2(): + pass + + @property + def func3(): + pass + +# end + + +# No error +try: + from nonexistent import Bar +except ImportError: + class Bar(object): + """This is a Bar replacement""" +# end + + +# No error +def with_feature(f): + """Some decorator""" + wrapper = f + if has_this_feature(f): + def wrapper(*args): + call_feature(args[0]) + return f(*args) + return wrapper +# end + + +# No error +try: + next +except NameError: + def next(iterator, default): + for item in iterator: + return item + return default +# end + + +# No error +def fn(): + pass + + +class Foo(): + """Class Foo""" + + def fn(): + + pass +# end + + +# No error +# comment +def c(): + pass + + +# comment + + +def d(): + pass + +# This is a +# ... multi-line comment + +# And this one is +# ... a second paragraph +# ... which spans on 3 lines + + +# Function `e` is below +# NOTE: Hey this is a testcase + +def e(): + pass + + +def fn(): + print() + + # comment + + print() + + print() + +# Comment 1 + +# Comment 2 + + +# Comment 3 + +def fn2(): + + pass +# end + + +# no error +if __name__ == '__main__': + foo() +# end + + +# no error +defaults = {} +defaults.update({}) +# end + + +# no error +def foo(x): + classification = x + definitely = not classification +# end + + +# no error +def bar(): pass +def baz(): pass +# end + + +# no error +def foo(): + def bar(): pass + def baz(): pass +# end + + +# no error +from typing import overload +from typing import Union +# end + + +# no error +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +# end + + +# no error +def f(x: Union[int, str]) -> Union[int, str]: + return x +# end + + +# no error +from typing import Protocol + + +class C(Protocol): + @property + def f(self) -> int: ... + @property + def g(self) -> str: ... +# end + + +# no error +def f( + a, +): + pass +# end + + +# no error +if True: + class Class: + """Docstring""" + + def function(self): + ... +# end + + +# no error +if True: + def function(self): + ... +# end + + +# no error +@decorator +# comment +@decorator +def function(): + pass +# end + + +# no error +class Class: + def method(self): + if True: + def function(): + pass +# end + + +# no error +@decorator +async def function(data: None) -> None: + ... +# end + + +# no error +class Class: + def method(): + """docstring""" + # comment + def function(): + pass +# end + + +# no error +try: + if True: + # comment + class Class: + pass + +except: + pass +# end + + +# no error +def f(): + def f(): + pass +# end + + +# no error +class MyClass: + # comment + def method(self) -> None: + pass +# end + + +# no error +def function1(): + # Comment + def function2(): + pass +# end + + +# no error +async def function1(): + await function2() + async with function3(): + pass +# end + + +# no error +if ( + cond1 + + + + + and cond2 +): + pass +#end + + +# no error +async def function1(): + await function2() + async with function3(): + pass +# end + + +# no error +async def function1(): + await function2() + async with function3(): + pass +# end + + +# no error +async def function1(): + await function2() + async with function3(): + pass +# end + + +# no error +class Test: + async + + def a(self): pass +# end + + +# no error +class Test: + def a(): + pass +# wrongly indented comment + + def b(): + pass +# end + + +# E301 +class Class(object): + + def func1(): + pass + def func2(): + pass +# end + + +# E301 +class Class: + + def fn1(): + pass + # comment + def fn2(): + pass +# end + + +# E302 +"""Main module.""" +def fn(): + pass +# end + + +# E302 +import sys +def get_sys_path(): + return sys.path +# end + + +# E302 +def a(): + pass + +def b(): + pass +# end + + +# E302 +def a(): + pass + +# comment + +def b(): + pass +# end + + +# E302 +def a(): + pass + +async def b(): + pass +# end + + +# E302 +async def x(): + pass + +async def x(y: int = 1): + pass +# end + + +# E302 +def bar(): + pass +def baz(): pass +# end + + +# E302 +def bar(): pass +def baz(): + pass +# end + + +# E302 +def f(): + pass + +# comment +@decorator +def g(): + pass +# end + + +# E303 +def fn(): + _ = None + + + # arbitrary comment + + def inner(): # E306 not expected (pycodestyle detects E306) + pass +# end + + +# E303 +def fn(): + _ = None + + + # arbitrary comment + def inner(): # E306 not expected (pycodestyle detects E306) + pass +# end + + +# E303 +print() + + + +print() +# end + + +# E303:5:1 +print() + + + +# comment + +print() +# end + + +# E303:5:5 E303:8:5 +def a(): + print() + + + # comment + + + # another comment + + print() +# end + + +# E303 +#!python + + + +"""This class docstring comes on line 5. +It gives error E303: too many blank lines (3) +""" +# end + + +# E303 +class Class: + def a(self): + pass + + + def b(self): + pass +# end + + +# E303 +if True: + a = 1 + + + a = 2 +# end + + +# E303 +class Test: + + + # comment + + + # another comment + + def test(self): pass +# end + + +# E303 +class Test: + def a(self): + pass + +# wrongly indented comment + + + def b(self): + pass +# end + + +# E304 +@decorator + +def function(): + pass +# end + + +# E304 +@decorator + +# comment E304 not expected +def function(): + pass +# end + + +# E304 +@decorator + +# comment E304 not expected + + +# second comment E304 not expected +def function(): + pass +# end + + +# E305:7:1 +def fn(): + print() + + # comment + + # another comment +fn() +# end + + +# E305 +class Class(): + pass + + # comment + + # another comment +a = 1 +# end + + +# E305:8:1 +def fn(): + print() + + # comment + + # another comment + +try: + fn() +except Exception: + pass +# end + + +# E305:5:1 +def a(): + print() + +# Two spaces before comments, too. +if a(): + a() +# end + + +#: E305:8:1 +# Example from https://github.com/PyCQA/pycodestyle/issues/400 +import stuff + + +def main(): + blah, blah + +if __name__ == '__main__': + main() +# end + + +# E306:3:5 +def a(): + x = 1 + def b(): + pass +# end + + +#: E306:3:5 +async def a(): + x = 1 + def b(): + pass +# end + + +#: E306:3:5 E306:5:9 +def a(): + x = 2 + def b(): + x = 1 + def c(): + pass +# end + + +# E306:3:5 E306:6:5 +def a(): + x = 1 + class C: + pass + x = 2 + def b(): + pass +# end + + +# E306 +def foo(): + def bar(): + pass + def baz(): pass +# end + + +# E306:3:5 +def foo(): + def bar(): pass + def baz(): + pass +# end + + +# E306 +def a(): + x = 2 + @decorator + def b(): + pass +# end + + +# E306 +def a(): + x = 2 + @decorator + async def b(): + pass +# end + + +# E306 +def a(): + x = 2 + async def b(): + pass +# end diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index e28c07e44aff1f..dc72a4834e99f1 100644 --- a/crates/ruff_linter/src/checkers/logical_lines.rs +++ b/crates/ruff_linter/src/checkers/logical_lines.rs @@ -1,3 +1,4 @@ +use crate::line_width::IndentWidth; use ruff_diagnostics::Diagnostic; use ruff_python_codegen::Stylist; use ruff_python_parser::lexer::LexResult; @@ -15,11 +16,11 @@ use crate::rules::pycodestyle::rules::logical_lines::{ use crate::settings::LinterSettings; /// Return the amount of indentation, expanding tabs to the next multiple of the settings' tab size. -fn expand_indent(line: &str, settings: &LinterSettings) -> usize { +pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize { let line = line.trim_end_matches(['\n', '\r']); let mut indent = 0; - let tab_size = settings.tab_size.as_usize(); + let tab_size = indent_width.as_usize(); for c in line.bytes() { match c { b'\t' => indent = (indent / tab_size) * tab_size + tab_size, @@ -85,7 +86,7 @@ pub(crate) fn check_logical_lines( TextRange::new(locator.line_start(first_token.start()), first_token.start()) }; - let indent_level = expand_indent(locator.slice(range), settings); + let indent_level = expand_indent(locator.slice(range), settings.tab_size); let indent_size = 4; diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index 26558aa25277ac..27662f02e6d73c 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -4,6 +4,7 @@ use std::path::Path; use ruff_notebook::CellOffsets; use ruff_python_ast::PySourceType; +use ruff_python_codegen::Stylist; use ruff_python_parser::lexer::LexResult; use ruff_python_parser::Tok; @@ -14,6 +15,7 @@ use ruff_source_file::Locator; use crate::directives::TodoComment; use crate::lex::docstring_detection::StateMachine; use crate::registry::{AsRule, Rule}; +use crate::rules::pycodestyle::rules::BlankLinesChecker; use crate::rules::ruff::rules::Context; use crate::rules::{ eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat, @@ -21,17 +23,37 @@ use crate::rules::{ }; use crate::settings::LinterSettings; +#[allow(clippy::too_many_arguments)] pub(crate) fn check_tokens( tokens: &[LexResult], path: &Path, locator: &Locator, indexer: &Indexer, + stylist: &Stylist, settings: &LinterSettings, source_type: PySourceType, cell_offsets: Option<&CellOffsets>, ) -> Vec { let mut diagnostics: Vec = vec![]; + if settings.rules.any_enabled(&[ + Rule::BlankLineBetweenMethods, + Rule::BlankLinesTopLevel, + Rule::TooManyBlankLines, + Rule::BlankLineAfterDecorator, + Rule::BlankLinesAfterFunctionOrClass, + Rule::BlankLinesBeforeNestedDefinition, + ]) { + let mut blank_lines_checker = BlankLinesChecker::default(); + blank_lines_checker.check_lines( + tokens, + locator, + stylist, + settings.tab_size, + &mut diagnostics, + ); + } + if settings.rules.enabled(Rule::BlanketNOQA) { pygrep_hooks::rules::blanket_noqa(&mut diagnostics, indexer, locator); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 41e0d4f5375afc..4f880e598727ca 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -137,6 +137,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword), #[allow(deprecated)] (Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword), + (Pycodestyle, "E301") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineBetweenMethods), + (Pycodestyle, "E302") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesTopLevel), + (Pycodestyle, "E303") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyBlankLines), + (Pycodestyle, "E304") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLineAfterDecorator), + (Pycodestyle, "E305") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesAfterFunctionOrClass), + (Pycodestyle, "E306") => (RuleGroup::Preview, rules::pycodestyle::rules::BlankLinesBeforeNestedDefinition), (Pycodestyle, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine), (Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile), (Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong), diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 0196aeb9336286..e5a4287f673ed5 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -109,6 +109,7 @@ pub fn check_path( path, locator, indexer, + stylist, settings, source_type, source_kind.as_ipy_notebook().map(Notebook::cell_offsets), diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 21499e96084928..1b59f90419bd39 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -264,6 +264,11 @@ impl Rule { | Rule::BadQuotesMultilineString | Rule::BlanketNOQA | Rule::BlanketTypeIgnore + | Rule::BlankLineAfterDecorator + | Rule::BlankLineBetweenMethods + | Rule::BlankLinesAfterFunctionOrClass + | Rule::BlankLinesBeforeNestedDefinition + | Rule::BlankLinesTopLevel | Rule::CommentedOutCode | Rule::EmptyComment | Rule::ExtraneousParentheses @@ -296,6 +301,7 @@ impl Rule { | Rule::ShebangNotFirstLine | Rule::SingleLineImplicitStringConcatenation | Rule::TabIndentation + | Rule::TooManyBlankLines | Rule::TrailingCommaOnBareTuple | Rule::TypeCommentInStub | Rule::UselessSemicolon diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 317993e97b65d4..5589733bef21db 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -136,6 +136,13 @@ mod tests { Path::new("E25.py") )] #[test_case(Rule::MissingWhitespaceAroundParameterEquals, Path::new("E25.py"))] + #[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<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); 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 new file mode 100644 index 00000000000000..cd3e23b5024d75 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -0,0 +1,896 @@ +use itertools::Itertools; +use std::cmp::Ordering; +use std::num::NonZeroU32; +use std::slice::Iter; + +use ruff_diagnostics::AlwaysFixableViolation; +use ruff_diagnostics::Diagnostic; +use ruff_diagnostics::Edit; +use ruff_diagnostics::Fix; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_codegen::Stylist; +use ruff_python_parser::lexer::LexResult; +use ruff_python_parser::lexer::LexicalError; +use ruff_python_parser::Tok; +use ruff_python_parser::TokenKind; +use ruff_source_file::{Locator, UniversalNewlines}; +use ruff_text_size::TextRange; +use ruff_text_size::TextSize; + +use crate::checkers::logical_lines::expand_indent; +use crate::line_width::IndentWidth; +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; + +/// ## What it does +/// Checks for missing blank lines between methods of a class. +/// +/// ## Why is this bad? +/// PEP 8 recommends exactly one blank line between methods of a class. +/// +/// ## Example +/// ```python +/// class MyClass(object): +/// def func1(): +/// pass +/// def func2(): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// class MyClass(object): +/// def func1(): +/// pass +/// +/// def func2(): +/// pass +/// ``` +/// +/// ## References +/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html) +#[violation] +pub struct BlankLineBetweenMethods; + +impl AlwaysFixableViolation for BlankLineBetweenMethods { + #[derive_message_formats] + fn message(&self) -> String { + format!("Expected {BLANK_LINES_METHOD_LEVEL:?} blank line, found 0") + } + + fn fix_title(&self) -> String { + "Add missing blank line".to_string() + } +} + +/// ## What it does +/// Checks for missing blank lines between top level functions and classes. +/// +/// ## Why is this bad? +/// PEP 8 recommends exactly two blank lines between top level functions and classes. +/// +/// ## Example +/// ```python +/// def func1(): +/// pass +/// def func2(): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// def func1(): +/// pass +/// +/// +/// def func2(): +/// pass +/// ``` +/// +/// ## References +/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html) +#[violation] +pub struct BlankLinesTopLevel { + actual_blank_lines: u32, +} + +impl AlwaysFixableViolation for BlankLinesTopLevel { + #[derive_message_formats] + fn message(&self) -> String { + let BlankLinesTopLevel { + actual_blank_lines: nb_blank_lines, + } = self; + + format!("Expected {BLANK_LINES_TOP_LEVEL:?} blank lines, found {nb_blank_lines}") + } + + fn fix_title(&self) -> String { + "Add missing blank line(s)".to_string() + } +} + +/// ## What it does +/// Checks for extraneous blank lines. +/// +/// ## Why is this bad? +/// PEP 8 recommends using blank lines as follows: +/// - No more than two blank lines between top-level statements. +/// - No more than one blank line between non-top-level statements. +/// +/// ## Example +/// ```python +/// def func1(): +/// pass +/// +/// +/// +/// def func2(): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// def func1(): +/// pass +/// +/// +/// def func2(): +/// pass +/// ``` +/// +/// ## References +/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html) +#[violation] +pub struct TooManyBlankLines { + actual_blank_lines: u32, +} + +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})") + } + + fn fix_title(&self) -> String { + "Remove extraneous blank line(s)".to_string() + } +} + +/// ## What it does +/// Checks for extraneous blank line(s) after function decorators. +/// +/// ## Why is this bad? +/// There should be no blank lines between a decorator and the object it is decorating. +/// +/// ## Example +/// ```python +/// class User(object): +/// +/// @property +/// +/// def name(self): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// class User(object): +/// +/// @property +/// def name(self): +/// pass +/// ``` +/// +/// ## References +/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [Flake 8 rule](https://www.flake8rules.com/rules/E304.html) +#[violation] +pub struct BlankLineAfterDecorator { + actual_blank_lines: u32, +} + +impl AlwaysFixableViolation for BlankLineAfterDecorator { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Blank lines found after function decorator ({lines})", + lines = self.actual_blank_lines + ) + } + + fn fix_title(&self) -> String { + "Remove extraneous blank line(s)".to_string() + } +} + +/// ## What it does +/// Checks for missing blank lines after the end of function or class. +/// +/// ## Why is this bad? +/// PEP 8 recommends using blank lines as following: +/// - Two blank lines are expected between functions and classes +/// - One blank line is expected between methods of a class. +/// +/// ## Example +/// ```python +/// class User(object): +/// pass +/// user = User() +/// ``` +/// +/// Use instead: +/// ```python +/// class User(object): +/// pass +/// +/// +/// user = User() +/// ``` +/// +/// ## References +/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html) +#[violation] +pub struct BlankLinesAfterFunctionOrClass { + actual_blank_lines: u32, +} + +impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass { + #[derive_message_formats] + fn message(&self) -> String { + let BlankLinesAfterFunctionOrClass { + actual_blank_lines: blank_lines, + } = self; + format!("Expected 2 blank lines after class or function definition, found ({blank_lines})") + } + + fn fix_title(&self) -> String { + "Add missing blank line(s)".to_string() + } +} + +/// ## What it does +/// Checks for 1 blank line between nested function or class definitions. +/// +/// ## Why is this bad? +/// PEP 8 recommends using blank lines as following: +/// - Two blank lines are expected between functions and classes +/// - One blank line is expected between methods of a class. +/// +/// ## Example +/// ```python +/// def outer(): +/// def inner(): +/// pass +/// def inner2(): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// def outer(): +/// def inner(): +/// pass +/// +/// def inner2(): +/// pass +/// ``` +/// +/// ## References +/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) +/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html) +#[violation] +pub struct BlankLinesBeforeNestedDefinition; + +impl AlwaysFixableViolation for BlankLinesBeforeNestedDefinition { + #[derive_message_formats] + fn message(&self) -> String { + format!("Expected 1 blank line before a nested definition, found 0") + } + + fn fix_title(&self) -> String { + "Add missing blank line".to_string() + } +} + +#[derive(Debug)] +struct LogicalLineInfo { + kind: LogicalLineKind, + first_token_range: TextRange, + + // The token's kind right before the newline ending the logical line. + last_token: TokenKind, + + // The end of the logical line including the newline. + logical_line_end: TextSize, + + // `true` if this is not a blank but only consists of a comment. + is_comment_only: bool, + + /// `true` if the line is a string only (including trivia tokens) line, which is a docstring if coming right after a class/function definition. + is_docstring: bool, + + /// The indentation length in columns. See [`expand_indent`] for the computation of the indent. + indent_length: usize, + + /// The number of blank lines preceding the current line. + blank_lines: BlankLines, + + /// The maximum number of consecutive blank lines between the current line + /// and the previous non-comment logical line. + /// One of its main uses is to allow a comments to directly precede or follow a class/function definition. + /// As such, `preceding_blank_lines` is used for rules that cannot trigger on comments (all rules except E303), + /// and `blank_lines` is used for the rule that can trigger on comments (E303). + preceding_blank_lines: BlankLines, +} + +/// Iterator that processes tokens until a full logical line (or comment line) is "built". +/// It then returns characteristics of that logical line (see `LogicalLineInfo`). +struct LinePreprocessor<'a> { + tokens: Iter<'a, Result<(Tok, TextRange), LexicalError>>, + locator: &'a Locator<'a>, + indent_width: IndentWidth, + /// The start position of the next logical line. + line_start: TextSize, + /// Maximum number of consecutive blank lines between the current line and the previous non-comment logical line. + /// One of its main uses is to allow a comment to directly precede a class/function definition. + max_preceding_blank_lines: BlankLines, +} + +impl<'a> LinePreprocessor<'a> { + fn new( + tokens: &'a [LexResult], + locator: &'a Locator, + indent_width: IndentWidth, + ) -> LinePreprocessor<'a> { + LinePreprocessor { + tokens: tokens.iter(), + locator, + line_start: TextSize::new(0), + max_preceding_blank_lines: BlankLines::Zero, + indent_width, + } + } +} + +impl<'a> Iterator for LinePreprocessor<'a> { + type Item = LogicalLineInfo; + + fn next(&mut self) -> Option { + let mut line_is_comment_only = true; + let mut is_docstring = false; + // Number of consecutive blank lines directly preceding this logical line. + let mut blank_lines = BlankLines::Zero; + let mut first_logical_line_token: Option<(LogicalLineKind, TextRange)> = None; + let mut last_token: TokenKind = TokenKind::EndOfFile; + let mut parens = 0u32; + + while let Some(result) = self.tokens.next() { + let Ok((token, range)) = result else { + continue; + }; + + if matches!(token, Tok::Indent | Tok::Dedent) { + continue; + } + + let token_kind = TokenKind::from_token(token); + + let (logical_line_kind, first_token_range) = if let Some(first_token_range) = + first_logical_line_token + { + first_token_range + } + // At the start of the line... + else { + // An empty line + if token_kind == TokenKind::NonLogicalNewline { + blank_lines.add(*range); + + self.line_start = range.end(); + + continue; + } + + is_docstring = token_kind == TokenKind::String; + + let logical_line_kind = match token_kind { + TokenKind::Class => LogicalLineKind::Class, + TokenKind::Comment => LogicalLineKind::Comment, + TokenKind::At => LogicalLineKind::Decorator, + TokenKind::Def => LogicalLineKind::Function, + // Lookahead to distinguish `async def` from `async with`. + TokenKind::Async + if matches!(self.tokens.as_slice().first(), Some(Ok((Tok::Def, _)))) => + { + LogicalLineKind::Function + } + _ => LogicalLineKind::Other, + }; + + first_logical_line_token = Some((logical_line_kind, *range)); + + (logical_line_kind, *range) + }; + + if !token_kind.is_trivia() { + line_is_comment_only = false; + } + + // A docstring line is composed only of the docstring (TokenKind::String) and trivia tokens. + // (If a comment follows a docstring, we still count the line as a docstring) + if token_kind != TokenKind::String && !token_kind.is_trivia() { + is_docstring = false; + } + + match token_kind { + TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => { + parens = parens.saturating_add(1); + } + TokenKind::Rbrace | TokenKind::Rpar | TokenKind::Rsqb => { + parens = parens.saturating_sub(1); + } + TokenKind::Newline | TokenKind::NonLogicalNewline if parens == 0 => { + let indent_range = TextRange::new(self.line_start, first_token_range.start()); + + let indent_length = + expand_indent(self.locator.slice(indent_range), self.indent_width); + + self.max_preceding_blank_lines = + self.max_preceding_blank_lines.max(blank_lines); + + let logical_line = LogicalLineInfo { + kind: logical_line_kind, + first_token_range, + last_token, + logical_line_end: range.end(), + is_comment_only: line_is_comment_only, + is_docstring, + indent_length, + blank_lines, + preceding_blank_lines: self.max_preceding_blank_lines, + }; + + // Reset the blank lines after a non-comment only line. + if !line_is_comment_only { + self.max_preceding_blank_lines = BlankLines::Zero; + } + + // Set the start for the next logical line. + self.line_start = range.end(); + + return Some(logical_line); + } + _ => {} + } + + last_token = token_kind; + } + + None + } +} + +#[derive(Clone, Copy, Debug, Default)] +enum BlankLines { + /// No blank lines + #[default] + Zero, + + /// One or more blank lines + Many { count: NonZeroU32, range: TextRange }, +} + +impl BlankLines { + fn add(&mut self, line_range: TextRange) { + match self { + BlankLines::Zero => { + *self = BlankLines::Many { + count: NonZeroU32::MIN, + range: line_range, + } + } + BlankLines::Many { count, range } => { + assert_eq!(range.end(), line_range.start()); + *count = count.saturating_add(1); + *range = TextRange::new(range.start(), line_range.end()); + } + } + } + + fn count(&self) -> u32 { + match self { + BlankLines::Zero => 0, + BlankLines::Many { count, .. } => count.get(), + } + } + + fn range(&self) -> Option { + match self { + BlankLines::Zero => None, + BlankLines::Many { range, .. } => Some(*range), + } + } +} + +impl PartialEq for BlankLines { + fn eq(&self, other: &u32) -> bool { + self.partial_cmp(other) == Some(Ordering::Equal) + } +} + +impl PartialOrd for BlankLines { + fn partial_cmp(&self, other: &u32) -> Option { + self.count().partial_cmp(other) + } +} + +impl PartialOrd for BlankLines { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BlankLines { + fn cmp(&self, other: &Self) -> Ordering { + self.count().cmp(&other.count()) + } +} + +impl PartialEq for BlankLines { + fn eq(&self, other: &Self) -> bool { + self.count() == other.count() + } +} + +impl Eq for BlankLines {} + +#[derive(Copy, Clone, Debug, Default)] +enum Follows { + #[default] + Other, + Decorator, + Def, + Docstring, +} + +#[derive(Copy, Clone, Debug, Default)] +enum Status { + /// Stores the indent level where the nesting started. + Inside(usize), + /// This is used to rectify a Inside switched to a Outside because of a dedented comment. + CommentAfter(usize), + #[default] + Outside, +} + +impl Status { + fn update(&mut self, line: &LogicalLineInfo) { + match *self { + Status::Inside(nesting_indent) => { + if line.indent_length <= nesting_indent { + if line.is_comment_only { + *self = Status::CommentAfter(nesting_indent); + } else { + *self = Status::Outside; + } + } + } + Status::CommentAfter(indent) => { + if !line.is_comment_only { + if line.indent_length > indent { + *self = Status::Inside(indent); + } else { + *self = Status::Outside; + } + } + } + Status::Outside => { + // Nothing to do + } + } + } +} + +/// 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, +} + +impl BlankLinesChecker { + /// 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, + ) { + let mut prev_indent_length: Option = None; + let line_preprocessor = LinePreprocessor::new(tokens, locator, indent_width); + + for logical_line in line_preprocessor { + self.check_line( + &logical_line, + prev_indent_length, + locator, + stylist, + diagnostics, + ); + if !logical_line.is_comment_only { + prev_indent_length = Some(logical_line.indent_length); + } + } + } + + #[allow(clippy::nonminimal_bool)] + fn check_line( + &mut self, + line: &LogicalLineInfo, + 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) + && 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), + ))); + + diagnostics.push(diagnostic); + } + + 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); + } + + let expected_blank_lines = if line.indent_length > 0 { + BLANK_LINES_METHOD_LEVEL + } else { + BLANK_LINES_TOP_LEVEL + }; + + 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, + ); + + if let Some(blank_lines_range) = line.blank_lines.range() { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + stylist.line_ending().repeat(expected_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, + )) + }; + + diagnostic.set_fix(fix); + + diagnostics.push(diagnostic); + } + + 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()), + ))); + } + + 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); + + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + stylist.line_ending().to_string(), + 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; + } + + 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); + } + } + } +} + +#[derive(Copy, Clone, Debug)] +enum LogicalLineKind { + /// The clause header of a class definition + Class, + /// A decorator + Decorator, + /// The clause header of a function + Function, + /// A comment only line + Comment, + /// Any other statement or clause header + Other, +} + +impl LogicalLineKind { + fn is_top_level(self) -> bool { + matches!( + self, + LogicalLineKind::Class | LogicalLineKind::Function | LogicalLineKind::Decorator + ) + } +} diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs index 327d81f02409c8..686b6bdc2c5b68 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs @@ -2,6 +2,7 @@ pub(crate) use ambiguous_class_name::*; pub(crate) use ambiguous_function_name::*; pub(crate) use ambiguous_variable_name::*; pub(crate) use bare_except::*; +pub(crate) use blank_lines::*; pub(crate) use compound_statements::*; pub(crate) use doc_line_too_long::*; pub(crate) use errors::*; @@ -23,6 +24,7 @@ mod ambiguous_class_name; mod ambiguous_function_name; mod ambiguous_variable_name; mod bare_except; +mod blank_lines; mod compound_statements; mod doc_line_too_long; mod errors; diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30.py.snap new file mode 100644 index 00000000000000..483170ced3def9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30.py.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.py:444:5: E301 [*] Expected 1 blank line, found 0 + | +442 | def func1(): +443 | pass +444 | def func2(): + | ^^^ E301 +445 | pass +446 | # end + | + = help: Add missing blank line + +ℹ Safe fix +441 441 | +442 442 | def func1(): +443 443 | pass + 444 |+ +444 445 | def func2(): +445 446 | pass +446 447 | # end + +E30.py:455:5: E301 [*] Expected 1 blank line, found 0 + | +453 | pass +454 | # comment +455 | def fn2(): + | ^^^ E301 +456 | pass +457 | # end + | + = help: Add missing blank line + +ℹ Safe fix +451 451 | +452 452 | def fn1(): +453 453 | pass + 454 |+ +454 455 | # comment +455 456 | def fn2(): +456 457 | pass + + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30.py.snap new file mode 100644 index 00000000000000..24311cccff3ed6 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30.py.snap @@ -0,0 +1,187 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.py:462:1: E302 [*] Expected 2 blank lines, found 0 + | +460 | # E302 +461 | """Main module.""" +462 | def fn(): + | ^^^ E302 +463 | pass +464 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +459 459 | +460 460 | # E302 +461 461 | """Main module.""" + 462 |+ + 463 |+ +462 464 | def fn(): +463 465 | pass +464 466 | # end + +E30.py:469:1: E302 [*] Expected 2 blank lines, found 0 + | +467 | # E302 +468 | import sys +469 | def get_sys_path(): + | ^^^ E302 +470 | return sys.path +471 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +466 466 | +467 467 | # E302 +468 468 | import sys + 469 |+ + 470 |+ +469 471 | def get_sys_path(): +470 472 | return sys.path +471 473 | # end + +E30.py:478:1: E302 [*] Expected 2 blank lines, found 1 + | +476 | pass +477 | +478 | def b(): + | ^^^ E302 +479 | pass +480 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +475 475 | def a(): +476 476 | pass +477 477 | + 478 |+ +478 479 | def b(): +479 480 | pass +480 481 | # end + +E30.py:489:1: E302 [*] Expected 2 blank lines, found 1 + | +487 | # comment +488 | +489 | def b(): + | ^^^ E302 +490 | pass +491 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +486 486 | +487 487 | # comment +488 488 | + 489 |+ +489 490 | def b(): +490 491 | pass +491 492 | # end + +E30.py:498:1: E302 [*] Expected 2 blank lines, found 1 + | +496 | pass +497 | +498 | async def b(): + | ^^^^^ E302 +499 | pass +500 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +495 495 | def a(): +496 496 | pass +497 497 | + 498 |+ +498 499 | async def b(): +499 500 | pass +500 501 | # end + +E30.py:507:1: E302 [*] Expected 2 blank lines, found 1 + | +505 | pass +506 | +507 | async def x(y: int = 1): + | ^^^^^ E302 +508 | pass +509 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +504 504 | async def x(): +505 505 | pass +506 506 | + 507 |+ +507 508 | async def x(y: int = 1): +508 509 | pass +509 510 | # end + +E30.py:515:1: E302 [*] Expected 2 blank lines, found 0 + | +513 | def bar(): +514 | pass +515 | def baz(): pass + | ^^^ E302 +516 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +512 512 | # E302 +513 513 | def bar(): +514 514 | pass + 515 |+ + 516 |+ +515 517 | def baz(): pass +516 518 | # end +517 519 | + +E30.py:521:1: E302 [*] Expected 2 blank lines, found 0 + | +519 | # E302 +520 | def bar(): pass +521 | def baz(): + | ^^^ E302 +522 | pass +523 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +518 518 | +519 519 | # E302 +520 520 | def bar(): pass + 521 |+ + 522 |+ +521 523 | def baz(): +522 524 | pass +523 525 | # end + +E30.py:531:1: E302 [*] Expected 2 blank lines, found 1 + | +530 | # comment +531 | @decorator + | ^ E302 +532 | def g(): +533 | pass + | + = help: Add missing blank line(s) + +ℹ Safe fix +527 527 | def f(): +528 528 | pass +529 529 | + 530 |+ + 531 |+ +530 532 | # comment +531 533 | @decorator +532 534 | def g(): + + 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 new file mode 100644 index 00000000000000..e6d6555838263c --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap @@ -0,0 +1,215 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.py:542:5: E303 [*] Too many blank lines (2) + | +542 | # arbitrary comment + | ^^^^^^^^^^^^^^^^^^^ E303 +543 | +544 | def inner(): # E306 not expected (pycodestyle detects E306) + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +538 538 | def fn(): +539 539 | _ = None +540 540 | +541 |- +542 541 | # arbitrary comment +543 542 | +544 543 | def inner(): # E306 not expected (pycodestyle detects E306) + +E30.py:554:5: E303 [*] Too many blank lines (2) + | +554 | # arbitrary comment + | ^^^^^^^^^^^^^^^^^^^ E303 +555 | def inner(): # E306 not expected (pycodestyle detects E306) +556 | pass + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +550 550 | def fn(): +551 551 | _ = None +552 552 | +553 |- +554 553 | # arbitrary comment +555 554 | def inner(): # E306 not expected (pycodestyle detects E306) +556 555 | pass + +E30.py:565:1: E303 [*] Too many blank lines (3) + | +565 | print() + | ^^^^^ E303 +566 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +561 561 | print() +562 562 | +563 563 | +564 |- +565 564 | print() +566 565 | # end +567 566 | + +E30.py:574:1: E303 [*] Too many blank lines (3) + | +574 | # comment + | ^^^^^^^^^ E303 +575 | +576 | print() + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +570 570 | print() +571 571 | +572 572 | +573 |- +574 573 | # comment +575 574 | +576 575 | print() + +E30.py:585:5: E303 [*] Too many blank lines (2) + | +585 | # comment + | ^^^^^^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +581 581 | def a(): +582 582 | print() +583 583 | +584 |- +585 584 | # comment +586 585 | +587 586 | + +E30.py:588:5: E303 [*] Too many blank lines (2) + | +588 | # another comment + | ^^^^^^^^^^^^^^^^^ E303 +589 | +590 | print() + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +584 584 | +585 585 | # comment +586 586 | +587 |- +588 587 | # another comment +589 588 | +590 589 | print() + +E30.py:599:1: E303 [*] Too many blank lines (3) + | +599 | / """This class docstring comes on line 5. +600 | | It gives error E303: too many blank lines (3) +601 | | """ + | |___^ E303 +602 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +595 595 | #!python +596 596 | +597 597 | +598 |- +599 598 | """This class docstring comes on line 5. +600 599 | It gives error E303: too many blank lines (3) +601 600 | """ + +E30.py:611:5: E303 [*] Too many blank lines (2) + | +611 | def b(self): + | ^^^ E303 +612 | pass +613 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +607 607 | def a(self): +608 608 | pass +609 609 | +610 |- +611 610 | def b(self): +612 611 | pass +613 612 | # end + +E30.py:621:5: E303 [*] Too many blank lines (2) + | +621 | a = 2 + | ^ E303 +622 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +617 617 | if True: +618 618 | a = 1 +619 619 | +620 |- +621 620 | a = 2 +622 621 | # end +623 622 | + +E30.py:629:5: E303 [*] Too many blank lines (2) + | +629 | # comment + | ^^^^^^^^^ E303 + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +625 625 | # E303 +626 626 | class Test: +627 627 | +628 |- +629 628 | # comment +630 629 | +631 630 | + +E30.py:632:5: E303 [*] Too many blank lines (2) + | +632 | # another comment + | ^^^^^^^^^^^^^^^^^ E303 +633 | +634 | def test(self): pass + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +628 628 | +629 629 | # comment +630 630 | +631 |- +632 631 | # another comment +633 632 | +634 633 | def test(self): pass + +E30.py:646:5: E303 [*] Too many blank lines (2) + | +646 | def b(self): + | ^^^ E303 +647 | pass +648 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +642 642 | +643 643 | # wrongly indented comment +644 644 | +645 |- +646 645 | def b(self): +647 646 | pass +648 647 | # end + + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E304_E30.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E304_E30.py.snap new file mode 100644 index 00000000000000..adf95ea1bc540d --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E304_E30.py.snap @@ -0,0 +1,65 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.py:654:1: E304 [*] Blank lines found after function decorator (1) + | +652 | @decorator +653 | +654 | def function(): + | ^^^ E304 +655 | pass +656 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +650 650 | +651 651 | # E304 +652 652 | @decorator +653 |- +654 653 | def function(): +655 654 | pass +656 655 | # end + +E30.py:663:1: E304 [*] Blank lines found after function decorator (1) + | +662 | # comment E304 not expected +663 | def function(): + | ^^^ E304 +664 | pass +665 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +658 658 | +659 659 | # E304 +660 660 | @decorator +661 |- +662 661 | # comment E304 not expected +663 662 | def function(): +664 663 | pass + +E30.py:675:1: E304 [*] Blank lines found after function decorator (2) + | +674 | # second comment E304 not expected +675 | def function(): + | ^^^ E304 +676 | pass +677 | # end + | + = help: Remove extraneous blank line(s) + +ℹ Safe fix +667 667 | +668 668 | # E304 +669 669 | @decorator +670 |- +671 670 | # comment E304 not expected +672 |- +673 |- +674 671 | # second comment E304 not expected +675 672 | def function(): +676 673 | pass + + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30.py.snap new file mode 100644 index 00000000000000..4addcca1859646 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30.py.snap @@ -0,0 +1,102 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.py:687:1: E305 [*] Expected 2 blank lines after class or function definition, found (1) + | +686 | # another comment +687 | fn() + | ^^ E305 +688 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +684 684 | # comment +685 685 | +686 686 | # another comment + 687 |+ + 688 |+ +687 689 | fn() +688 690 | # end +689 691 | + +E30.py:698:1: E305 [*] Expected 2 blank lines after class or function definition, found (1) + | +697 | # another comment +698 | a = 1 + | ^ E305 +699 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +695 695 | # comment +696 696 | +697 697 | # another comment + 698 |+ + 699 |+ +698 700 | a = 1 +699 701 | # end +700 702 | + +E30.py:710:1: E305 [*] Expected 2 blank lines after class or function definition, found (1) + | +708 | # another comment +709 | +710 | try: + | ^^^ E305 +711 | fn() +712 | except Exception: + | + = help: Add missing blank line(s) + +ℹ Safe fix +707 707 | +708 708 | # another comment +709 709 | + 710 |+ +710 711 | try: +711 712 | fn() +712 713 | except Exception: + +E30.py:722:1: E305 [*] Expected 2 blank lines after class or function definition, found (1) + | +721 | # Two spaces before comments, too. +722 | if a(): + | ^^ E305 +723 | a() +724 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +719 719 | print() +720 720 | +721 721 | # Two spaces before comments, too. + 722 |+ + 723 |+ +722 724 | if a(): +723 725 | a() +724 726 | # end + +E30.py:735:1: E305 [*] Expected 2 blank lines after class or function definition, found (1) + | +733 | blah, blah +734 | +735 | if __name__ == '__main__': + | ^^ E305 +736 | main() +737 | # end + | + = help: Add missing blank line(s) + +ℹ Safe fix +732 732 | def main(): +733 733 | blah, blah +734 734 | + 735 |+ +735 736 | if __name__ == '__main__': +736 737 | main() +737 738 | # end + + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30.py.snap new file mode 100644 index 00000000000000..c9a2629b06795c --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30.py.snap @@ -0,0 +1,223 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E30.py:743:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +741 | def a(): +742 | x = 1 +743 | def b(): + | ^^^ E306 +744 | pass +745 | # end + | + = help: Add missing blank line + +ℹ Safe fix +740 740 | # E306:3:5 +741 741 | def a(): +742 742 | x = 1 + 743 |+ +743 744 | def b(): +744 745 | pass +745 746 | # end + +E30.py:751:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +749 | async def a(): +750 | x = 1 +751 | def b(): + | ^^^ E306 +752 | pass +753 | # end + | + = help: Add missing blank line + +ℹ Safe fix +748 748 | #: E306:3:5 +749 749 | async def a(): +750 750 | x = 1 + 751 |+ +751 752 | def b(): +752 753 | pass +753 754 | # end + +E30.py:759:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +757 | def a(): +758 | x = 2 +759 | def b(): + | ^^^ E306 +760 | x = 1 +761 | def c(): + | + = help: Add missing blank line + +ℹ Safe fix +756 756 | #: E306:3:5 E306:5:9 +757 757 | def a(): +758 758 | x = 2 + 759 |+ +759 760 | def b(): +760 761 | x = 1 +761 762 | def c(): + +E30.py:761:9: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +759 | def b(): +760 | x = 1 +761 | def c(): + | ^^^ E306 +762 | pass +763 | # end + | + = help: Add missing blank line + +ℹ Safe fix +758 758 | x = 2 +759 759 | def b(): +760 760 | x = 1 + 761 |+ +761 762 | def c(): +762 763 | pass +763 764 | # end + +E30.py:769:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +767 | def a(): +768 | x = 1 +769 | class C: + | ^^^^^ E306 +770 | pass +771 | x = 2 + | + = help: Add missing blank line + +ℹ Safe fix +766 766 | # E306:3:5 E306:6:5 +767 767 | def a(): +768 768 | x = 1 + 769 |+ +769 770 | class C: +770 771 | pass +771 772 | x = 2 + +E30.py:772:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +770 | pass +771 | x = 2 +772 | def b(): + | ^^^ E306 +773 | pass +774 | # end + | + = help: Add missing blank line + +ℹ Safe fix +769 769 | class C: +770 770 | pass +771 771 | x = 2 + 772 |+ +772 773 | def b(): +773 774 | pass +774 775 | # end + +E30.py:781:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +779 | def bar(): +780 | pass +781 | def baz(): pass + | ^^^ E306 +782 | # end + | + = help: Add missing blank line + +ℹ Safe fix +778 778 | def foo(): +779 779 | def bar(): +780 780 | pass + 781 |+ +781 782 | def baz(): pass +782 783 | # end +783 784 | + +E30.py:788:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +786 | def foo(): +787 | def bar(): pass +788 | def baz(): + | ^^^ E306 +789 | pass +790 | # end + | + = help: Add missing blank line + +ℹ Safe fix +785 785 | # E306:3:5 +786 786 | def foo(): +787 787 | def bar(): pass + 788 |+ +788 789 | def baz(): +789 790 | pass +790 791 | # end + +E30.py:796:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +794 | def a(): +795 | x = 2 +796 | @decorator + | ^ E306 +797 | def b(): +798 | pass + | + = help: Add missing blank line + +ℹ Safe fix +793 793 | # E306 +794 794 | def a(): +795 795 | x = 2 + 796 |+ +796 797 | @decorator +797 798 | def b(): +798 799 | pass + +E30.py:805:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +803 | def a(): +804 | x = 2 +805 | @decorator + | ^ E306 +806 | async def b(): +807 | pass + | + = help: Add missing blank line + +ℹ Safe fix +802 802 | # E306 +803 803 | def a(): +804 804 | x = 2 + 805 |+ +805 806 | @decorator +806 807 | async def b(): +807 808 | pass + +E30.py:814:5: E306 [*] Expected 1 blank line before a nested definition, found 0 + | +812 | def a(): +813 | x = 2 +814 | async def b(): + | ^^^^^ E306 +815 | pass +816 | # end + | + = help: Add missing blank line + +ℹ Safe fix +811 811 | # E306 +812 812 | def a(): +813 813 | x = 2 + 814 |+ +814 815 | async def b(): +815 816 | pass +816 817 | # end + + diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 0cd2a8f14017f9..c41006b09e968b 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -1483,6 +1483,12 @@ mod tests { Rule::UnnecessaryEnumerate, Rule::MathConstant, Rule::PreviewTestRule, + Rule::BlankLineBetweenMethods, + Rule::BlankLinesTopLevel, + Rule::TooManyBlankLines, + Rule::BlankLineAfterDecorator, + Rule::BlankLinesAfterFunctionOrClass, + Rule::BlankLinesBeforeNestedDefinition, ]; #[allow(clippy::needless_pass_by_value)] diff --git a/ruff.schema.json b/ruff.schema.json index b01219b9154a5e..6addfdd85c8c6d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2838,6 +2838,14 @@ "E273", "E274", "E275", + "E3", + "E30", + "E301", + "E302", + "E303", + "E304", + "E305", + "E306", "E4", "E40", "E401", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index d4bf715dd9b1ec..234fb825e67a98 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -32,6 +32,11 @@ "bad-quotes-docstring", "bad-quotes-inline-string", "bad-quotes-multiline-string", + "blank-line-after-decorator", + "blank-line-between-methods", + "blank-lines-after-function-or-class", + "blank-lines-before-nested-definition", + "blank-lines-top-level", "explicit-string-concatenation", "indent-with-spaces", "indentation-with-invalid-multiple", @@ -68,6 +73,7 @@ "surrounding-whitespace", "tab-indentation", "too-few-spaces-before-inline-comment", + "too-many-blank-lines", "too-many-boolean-expressions", "trailing-comma-on-bare-tuple", "triple-single-quotes",