From 85bd0b415d30d389cef2e1d1405dd54b8e85e142 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ho=C3=ABl=20Bagard?=
 <34478245+hoel-bagard@users.noreply.github.com>
Date: Fri, 9 Feb 2024 03:35:08 +0900
Subject: [PATCH] [`pycodestyle`] Add blank line(s) rules (`E301`, `E302`,
 `E303`, `E304`, `E305`, `E306`) (#9266)

Co-authored-by: Micha Reiser <micha@reiser.io>
---
 .../test/fixtures/pycodestyle/E30.py          | 816 ++++++++++++++++
 .../ruff_linter/src/checkers/logical_lines.rs |   7 +-
 crates/ruff_linter/src/checkers/tokens.rs     |  22 +
 crates/ruff_linter/src/codes.rs               |   6 +
 crates/ruff_linter/src/linter.rs              |   1 +
 crates/ruff_linter/src/registry.rs            |   6 +
 .../ruff_linter/src/rules/pycodestyle/mod.rs  |   7 +
 .../rules/pycodestyle/rules/blank_lines.rs    | 896 ++++++++++++++++++
 .../src/rules/pycodestyle/rules/mod.rs        |   2 +
 ...ules__pycodestyle__tests__E301_E30.py.snap |  44 +
 ...ules__pycodestyle__tests__E302_E30.py.snap | 187 ++++
 ...ules__pycodestyle__tests__E303_E30.py.snap | 215 +++++
 ...ules__pycodestyle__tests__E304_E30.py.snap |  65 ++
 ...ules__pycodestyle__tests__E305_E30.py.snap | 102 ++
 ...ules__pycodestyle__tests__E306_E30.py.snap | 223 +++++
 crates/ruff_workspace/src/configuration.rs    |   6 +
 ruff.schema.json                              |   8 +
 scripts/check_docs_formatted.py               |   6 +
 18 files changed, 2616 insertions(+), 3 deletions(-)
 create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/E30.py
 create mode 100644 crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs
 create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E301_E30.py.snap
 create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E302_E30.py.snap
 create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E303_E30.py.snap
 create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E304_E30.py.snap
 create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E305_E30.py.snap
 create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E306_E30.py.snap

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<Diagnostic> {
     let mut diagnostics: Vec<Diagnostic> = 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<LogicalLineInfo> {
+        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<TextRange> {
+        match self {
+            BlankLines::Zero => None,
+            BlankLines::Many { range, .. } => Some(*range),
+        }
+    }
+}
+
+impl PartialEq<u32> for BlankLines {
+    fn eq(&self, other: &u32) -> bool {
+        self.partial_cmp(other) == Some(Ordering::Equal)
+    }
+}
+
+impl PartialOrd<u32> for BlankLines {
+    fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
+        self.count().partial_cmp(other)
+    }
+}
+
+impl PartialOrd for BlankLines {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        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<LogicalLineKind>,
+}
+
+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<Diagnostic>,
+    ) {
+        let mut prev_indent_length: Option<usize> = 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<usize>,
+        locator: &Locator,
+        stylist: &Stylist,
+        diagnostics: &mut Vec<Diagnostic>,
+    ) {
+        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",