From 21e56870572aff0547d8464485ecea5be63b5bae Mon Sep 17 00:00:00 2001 From: augustelalande Date: Mon, 12 Feb 2024 15:23:05 -0500 Subject: [PATCH 1/9] add class_newline rule --- .../ruff_linter/src/checkers/logical_lines.rs | 2 + crates/ruff_linter/src/codes.rs | 3 + crates/ruff_linter/src/registry.rs | 3 + .../src/rules/flake8_class_newline/mod.rs | 27 ++++++++ .../rules/missing_class_newline.rs | 64 +++++++++++++++++++ .../rules/flake8_class_newline/rules/mod.rs | 3 + crates/ruff_linter/src/rules/mod.rs | 1 + 7 files changed, 103 insertions(+) create mode 100644 crates/ruff_linter/src/rules/flake8_class_newline/mod.rs create mode 100644 crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs create mode 100644 crates/ruff_linter/src/rules/flake8_class_newline/rules/mod.rs diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index c216ce2f671a6..0ba0fdcc210fc 100644 --- a/crates/ruff_linter/src/checkers/logical_lines.rs +++ b/crates/ruff_linter/src/checkers/logical_lines.rs @@ -12,6 +12,7 @@ use crate::rules::pycodestyle::rules::logical_lines::{ whitespace_around_keywords, whitespace_around_named_parameter_equals, whitespace_before_comment, whitespace_before_parameters, LogicalLines, TokenFlags, }; +use crate::rules::flake8_class_newline::rules::missing_class_newline; use crate::settings::LinterSettings; /// Return the amount of indentation, expanding tabs to the next multiple of 8. @@ -63,6 +64,7 @@ pub(crate) fn check_logical_lines( if line.flags().contains(TokenFlags::KEYWORD) { whitespace_around_keywords(&line, &mut context); missing_whitespace_after_keyword(&line, &mut context); + missing_class_newline(&line, &prev_line, &mut context); } if line.flags().contains(TokenFlags::COMMENT) { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index fd0b85727bbec..eba632fa9ea6a 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -972,6 +972,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Logging, "007") => (RuleGroup::Preview, rules::flake8_logging::rules::ExceptionWithoutExcInfo), (Flake8Logging, "009") => (RuleGroup::Preview, rules::flake8_logging::rules::UndocumentedWarn), + // flake8-class-newline + (Flake8ClassNewline, "100") => (RuleGroup::Preview, rules::flake8_class_newline::rules::ClassNewline), + _ => return None, }) } diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 2e6fcc199580e..387d13201b246 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -82,6 +82,9 @@ pub enum Linter { /// [flake8-builtins](https://pypi.org/project/flake8-builtins/) #[prefix = "A"] Flake8Builtins, + /// [flake8-class-newline](https://pypi.org/project/flake8-class-newline/) + #[prefix = "CNL"] + Flake8ClassNewline, /// [flake8-commas](https://pypi.org/project/flake8-commas/) #[prefix = "COM"] Flake8Commas, diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs b/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs new file mode 100644 index 0000000000000..783c19a0e512c --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs @@ -0,0 +1,27 @@ +//! Rules from [flake8-class-newline](https://pypi.org/project/flake8-class-newline/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::registry::Rule; + use crate::test::test_path; + use crate::{assert_messages, settings}; + + #[test_case(Path::new("COM81.py"))] + fn rules(path: &Path) -> Result<()> { + let snapshot = path.to_string_lossy().into_owned(); + let diagnostics = test_path( + Path::new("flake8_class_newline").join(path).as_path(), + &settings::LinterSettings::for_rules(vec![ + Rule::ClassNewline, + ]), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs new file mode 100644 index 0000000000000..70e193a398ef4 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs @@ -0,0 +1,64 @@ +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_parser::TokenKind; +use ruff_text_size::Ranged; + +use crate::checkers::logical_lines::LogicalLinesContext; +use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine}; + +/// ## What it does +/// Checks for a newline after a class definition. +/// +/// ## Why is this important? +/// Adhering to PEP 8 guidelines helps maintain consistent and readable code. +/// Having a newline after a class definition is recommended by PEP 8. +/// +/// ## Example +/// ```python +/// class MyClass: +/// def method(self): +/// return 'example' +/// ``` +/// +/// Use instead: +/// ```python +/// class MyClass: +/// +/// def method(self): +/// return 'example' +/// ``` +/// +/// ## References +/// - [PEP 8 - Blank Lines](https://peps.python.org/pep-0008/#blank-lines) +#[violation] +pub struct ClassNewline; + +impl Violation for ClassNewline { + #[derive_message_formats] + fn message(&self) -> String { + format!("Class definition does not have a new line") + } +} + +/// CNL100 +pub(crate) fn missing_class_newline( + line: &LogicalLine, + prev_line: Option<&LogicalLine>, + context: &mut LogicalLinesContext +) { + if let Some(prev_line) = prev_line { + if let Some(token) = line.tokens_trimmed().first() { + if matches!(token.kind(), TokenKind::Def | TokenKind::At) { + if let Some(token) = prev_line.tokens_trimmed().first() { + if matches!(token.kind(), TokenKind::Class) { + let diagnostic = Diagnostic::new( + ClassNewline, + token.range(), + ); + context.push_diagnostic(diagnostic); + } + } + } + } + } +} diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_class_newline/rules/mod.rs new file mode 100644 index 0000000000000..7e255aa238ca3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_class_newline/rules/mod.rs @@ -0,0 +1,3 @@ +pub(crate) use missing_class_newline::*; + +mod missing_class_newline; diff --git a/crates/ruff_linter/src/rules/mod.rs b/crates/ruff_linter/src/rules/mod.rs index 64b0128cae164..00e8b796736b2 100644 --- a/crates/ruff_linter/src/rules/mod.rs +++ b/crates/ruff_linter/src/rules/mod.rs @@ -9,6 +9,7 @@ pub mod flake8_blind_except; pub mod flake8_boolean_trap; pub mod flake8_bugbear; pub mod flake8_builtins; +pub mod flake8_class_newline; pub mod flake8_commas; pub mod flake8_comprehensions; pub mod flake8_copyright; From 4b2c19474979be880dc366beb296e83391a35994 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 18 Feb 2024 16:27:42 -0500 Subject: [PATCH 2/9] implement MissingClassNewLine --- .../ruff_linter/src/checkers/logical_lines.rs | 2 - crates/ruff_linter/src/checkers/tokens.rs | 13 +- crates/ruff_linter/src/codes.rs | 2 +- crates/ruff_linter/src/registry.rs | 3 +- .../src/rules/flake8_class_newline/mod.rs | 25 ---- .../rules/missing_class_newline.rs | 130 +++++++++++------- .../rules/pycodestyle/rules/blank_lines.rs | 30 ++-- 7 files changed, 109 insertions(+), 96 deletions(-) diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index 45d04bc825902..dc72a4834e99f 100644 --- a/crates/ruff_linter/src/checkers/logical_lines.rs +++ b/crates/ruff_linter/src/checkers/logical_lines.rs @@ -13,7 +13,6 @@ use crate::rules::pycodestyle::rules::logical_lines::{ whitespace_around_keywords, whitespace_around_named_parameter_equals, whitespace_before_comment, whitespace_before_parameters, LogicalLines, TokenFlags, }; -use crate::rules::flake8_class_newline::rules::missing_class_newline; use crate::settings::LinterSettings; /// Return the amount of indentation, expanding tabs to the next multiple of the settings' tab size. @@ -66,7 +65,6 @@ pub(crate) fn check_logical_lines( if line.flags().contains(TokenFlags::KEYWORD) { whitespace_around_keywords(&line, &mut context); missing_whitespace_after_keyword(&line, &mut context); - missing_class_newline(&line, &prev_line, &mut context); } if line.flags().contains(TokenFlags::COMMENT) { diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index c676645815669..1a0e273bbd178 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -15,7 +15,7 @@ use crate::directives::TodoComment; use crate::registry::{AsRule, Rule}; use crate::rules::pycodestyle::rules::BlankLinesChecker; use crate::rules::{ - eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat, + eradicate, flake8_class_newline, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff, }; use crate::settings::LinterSettings; @@ -209,6 +209,17 @@ pub(crate) fn check_tokens( flake8_fixme::rules::todos(&mut diagnostics, &todo_comments); } + if settings.rules.enabled(Rule::MissingClassNewLine) { + let mut blank_lines_checker = flake8_class_newline::rules::BlankLinesChecker::default(); + blank_lines_checker.check_lines( + tokens, + locator, + stylist, + settings.tab_size, + &mut diagnostics, + ); + } + diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())); diagnostics diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 1e2653250bc0d..7c5e2a17e8581 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1052,7 +1052,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Logging, "009") => (RuleGroup::Stable, rules::flake8_logging::rules::UndocumentedWarn), // flake8-class-newline - (Flake8ClassNewline, "100") => (RuleGroup::Preview, rules::flake8_class_newline::rules::ClassNewline), + (Flake8ClassNewLine, "100") => (RuleGroup::Preview, rules::flake8_class_newline::rules::MissingClassNewLine), _ => return None, }) diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 31a22d3e38a4f..3169f3d0705ff 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -84,7 +84,7 @@ pub enum Linter { Flake8Builtins, /// [flake8-class-newline](https://pypi.org/project/flake8-class-newline/) #[prefix = "CNL"] - Flake8ClassNewline, + Flake8ClassNewLine, /// [flake8-commas](https://pypi.org/project/flake8-commas/) #[prefix = "COM"] Flake8Commas, @@ -285,6 +285,7 @@ impl Rule { | Rule::LineContainsHack | Rule::LineContainsTodo | Rule::LineContainsXxx + | Rule::MissingClassNewLine | Rule::MissingSpaceAfterTodoColon | Rule::MissingTodoAuthor | Rule::MissingTodoColon diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs b/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs index 783c19a0e512c..18c25933c861d 100644 --- a/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs @@ -1,27 +1,2 @@ //! Rules from [flake8-class-newline](https://pypi.org/project/flake8-class-newline/). pub(crate) mod rules; - -#[cfg(test)] -mod tests { - use std::path::Path; - - use anyhow::Result; - use test_case::test_case; - - use crate::registry::Rule; - use crate::test::test_path; - use crate::{assert_messages, settings}; - - #[test_case(Path::new("COM81.py"))] - fn rules(path: &Path) -> Result<()> { - let snapshot = path.to_string_lossy().into_owned(); - let diagnostics = test_path( - Path::new("flake8_class_newline").join(path).as_path(), - &settings::LinterSettings::for_rules(vec![ - Rule::ClassNewline, - ]), - )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } -} diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs index 70e193a398ef4..fa058a8bc5f7c 100644 --- a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs +++ b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs @@ -1,64 +1,92 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +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_parser::TokenKind; -use ruff_text_size::Ranged; +use ruff_python_codegen::Stylist; +use ruff_python_parser::lexer::LexResult; +use ruff_source_file::{Locator}; +use ruff_text_size::TextSize; + +use crate::line_width::IndentWidth; +use crate::rules::pycodestyle::rules::{ + LogicalLineInfo, LogicalLineKind, LinePreprocessor, Status}; -use crate::checkers::logical_lines::LogicalLinesContext; -use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine}; -/// ## What it does -/// Checks for a newline after a class definition. -/// -/// ## Why is this important? -/// Adhering to PEP 8 guidelines helps maintain consistent and readable code. -/// Having a newline after a class definition is recommended by PEP 8. -/// -/// ## Example -/// ```python -/// class MyClass: -/// def method(self): -/// return 'example' -/// ``` -/// -/// Use instead: -/// ```python -/// class MyClass: -/// -/// def method(self): -/// return 'example' -/// ``` -/// -/// ## References -/// - [PEP 8 - Blank Lines](https://peps.python.org/pep-0008/#blank-lines) #[violation] -pub struct ClassNewline; +pub struct MissingClassNewLine; -impl Violation for ClassNewline { +impl AlwaysFixableViolation for MissingClassNewLine { #[derive_message_formats] fn message(&self) -> String { - format!("Class definition does not have a new line") + format!("Expected 1 blank line after class declaration, found 0") + } + + fn fix_title(&self) -> String { + "Add missing blank line".to_string() } } -/// CNL100 -pub(crate) fn missing_class_newline( - line: &LogicalLine, - prev_line: Option<&LogicalLine>, - context: &mut LogicalLinesContext -) { - if let Some(prev_line) = prev_line { - if let Some(token) = line.tokens_trimmed().first() { - if matches!(token.kind(), TokenKind::Def | TokenKind::At) { - if let Some(token) = prev_line.tokens_trimmed().first() { - if matches!(token.kind(), TokenKind::Class) { - let diagnostic = Diagnostic::new( - ClassNewline, - token.range(), - ); - context.push_diagnostic(diagnostic); - } - } - } + +#[derive(Copy, Clone, Debug, Default)] +enum Follows { + #[default] + Class, + Other, +} + + +/// Contains variables used for the linting of blank lines. +#[derive(Debug, Default)] +pub(crate) struct BlankLinesChecker { + follows: Follows, +} + +impl BlankLinesChecker { + pub(crate) fn check_lines( + &mut self, + tokens: &[LexResult], + locator: &Locator, + stylist: &Stylist, + indent_width: IndentWidth, + diagnostics: &mut Vec, + ) { + let line_preprocessor = LinePreprocessor::new(tokens, locator, indent_width); + + for logical_line in line_preprocessor { + self.check_new_line_after_class_declaration( + &logical_line, + locator, + stylist, + diagnostics + ); + } + } + + fn check_new_line_after_class_declaration( + &mut self, + line: &LogicalLineInfo, + locator: &Locator, + stylist: &Stylist, + diagnostics: &mut Vec, + ) { + if (matches!(self.follows, Follows::Class) && matches!(line.kind, LogicalLineKind::Function | LogicalLineKind::Decorator) && line.preceding_blank_lines == 0) { + let mut diagnostic = Diagnostic::new( + MissingClassNewLine, + 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); + } + + // Update the `self.follows` state based on the current line + match line.kind { + LogicalLineKind::Class => self.follows = Follows::Class, + _ => self.follows = Follows::Other, } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs index 59effe7f944da..144ed30f67f85 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -304,39 +304,39 @@ impl AlwaysFixableViolation for BlankLinesBeforeNestedDefinition { } #[derive(Debug)] -struct LogicalLineInfo { - kind: LogicalLineKind, - first_token_range: TextRange, +pub(crate) struct LogicalLineInfo { + pub(crate) kind: LogicalLineKind, + pub(crate) first_token_range: TextRange, // The token's kind right before the newline ending the logical line. - last_token: TokenKind, + pub(crate) last_token: TokenKind, // The end of the logical line including the newline. - logical_line_end: TextSize, + pub(crate) logical_line_end: TextSize, // `true` if this is not a blank but only consists of a comment. - is_comment_only: bool, + pub(crate) 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, + pub(crate) is_docstring: bool, /// The indentation length in columns. See [`expand_indent`] for the computation of the indent. - indent_length: usize, + pub(crate) indent_length: usize, /// The number of blank lines preceding the current line. - blank_lines: BlankLines, + pub(crate) 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, + pub(crate) 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> { +pub(crate) struct LinePreprocessor<'a> { tokens: Iter<'a, Result<(Tok, TextRange), LexicalError>>, locator: &'a Locator<'a>, indent_width: IndentWidth, @@ -348,7 +348,7 @@ struct LinePreprocessor<'a> { } impl<'a> LinePreprocessor<'a> { - fn new( + pub(crate) fn new( tokens: &'a [LexResult], locator: &'a Locator, indent_width: IndentWidth, @@ -482,7 +482,7 @@ impl<'a> Iterator for LinePreprocessor<'a> { } #[derive(Clone, Copy, Debug, Default)] -enum BlankLines { +pub(crate) enum BlankLines { /// No blank lines #[default] Zero, @@ -564,7 +564,7 @@ enum Follows { } #[derive(Copy, Clone, Debug, Default)] -enum Status { +pub(crate) 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. @@ -872,7 +872,7 @@ impl BlankLinesChecker { } #[derive(Copy, Clone, Debug)] -enum LogicalLineKind { +pub(crate) enum LogicalLineKind { /// The clause header of a class definition Class, /// A decorator From 8527cf0f600419800119e71a5e00e7db461caa76 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 18 Feb 2024 17:00:07 -0500 Subject: [PATCH 3/9] create test cases --- .../fixtures/flake8_class_newline/CNL100.py | 20 ++++++++++++++++ .../src/rules/flake8_class_newline/mod.rs | 23 +++++++++++++++++++ .../rules/missing_class_newline.rs | 3 +-- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_class_newline/CNL100.py diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_class_newline/CNL100.py b/crates/ruff_linter/resources/test/fixtures/flake8_class_newline/CNL100.py new file mode 100644 index 0000000000000..961f82c100512 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_class_newline/CNL100.py @@ -0,0 +1,20 @@ +class A: # CNL100 + def __init__(self): + pass + + +class B: # correct + + def __init__(self): + pass + + +class C: # CNL100 + async def foo(self): + pass + + +class D: # correct + + async def foo(self): + pass diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs b/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs index 18c25933c861d..a9b540075b2d4 100644 --- a/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_class_newline/mod.rs @@ -1,2 +1,25 @@ //! Rules from [flake8-class-newline](https://pypi.org/project/flake8-class-newline/). pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::registry::Rule; + use crate::test::test_path; + use crate::{assert_messages, settings}; + + #[test_case(Path::new("CNL100.py"))] + fn rules(path: &Path) -> Result<()> { + let snapshot = path.to_string_lossy().into_owned(); + let diagnostics = test_path( + Path::new("flake8_class_newline").join(path).as_path(), + &settings::LinterSettings::for_rule(Rule::MissingClassNewLine), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs index fa058a8bc5f7c..6c173276ef24d 100644 --- a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs +++ b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs @@ -6,11 +6,10 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_codegen::Stylist; use ruff_python_parser::lexer::LexResult; use ruff_source_file::{Locator}; -use ruff_text_size::TextSize; use crate::line_width::IndentWidth; use crate::rules::pycodestyle::rules::{ - LogicalLineInfo, LogicalLineKind, LinePreprocessor, Status}; + LogicalLineInfo, LogicalLineKind, LinePreprocessor}; #[violation] From 166d7ef6d6d8772a44acf44cf0499aee974dd667 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 18 Feb 2024 18:10:52 -0500 Subject: [PATCH 4/9] update schema --- ruff.schema.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ruff.schema.json b/ruff.schema.json index 369c973256cc5..3be8523d34a16 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2700,6 +2700,10 @@ "C9", "C90", "C901", + "CNL", + "CNL1", + "CNL10", + "CNL100", "COM", "COM8", "COM81", From 9607dd501eeeb95f2460fc0c710ec848fb97f477 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 18 Feb 2024 18:11:51 -0500 Subject: [PATCH 5/9] add class_newline snapshot --- ...lake8_class_newline__tests__CNL100.py.snap | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 crates/ruff_linter/src/rules/flake8_class_newline/snapshots/ruff_linter__rules__flake8_class_newline__tests__CNL100.py.snap diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/snapshots/ruff_linter__rules__flake8_class_newline__tests__CNL100.py.snap b/crates/ruff_linter/src/rules/flake8_class_newline/snapshots/ruff_linter__rules__flake8_class_newline__tests__CNL100.py.snap new file mode 100644 index 0000000000000..06b7d0f96e2b3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_class_newline/snapshots/ruff_linter__rules__flake8_class_newline__tests__CNL100.py.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_linter/src/rules/flake8_class_newline/mod.rs +--- +CNL100.py:2:5: CNL100 [*] Expected 1 blank line after class declaration, found 0 + | +1 | class A: # CNL100 +2 | def __init__(self): + | ^^^ CNL100 +3 | pass + | + = help: Add missing blank line + +ℹ Safe fix +1 1 | class A: # CNL100 + 2 |+ +2 3 | def __init__(self): +3 4 | pass +4 5 | + +CNL100.py:13:5: CNL100 [*] Expected 1 blank line after class declaration, found 0 + | +12 | class C: # CNL100 +13 | async def foo(self): + | ^^^^^ CNL100 +14 | pass + | + = help: Add missing blank line + +ℹ Safe fix +10 10 | +11 11 | +12 12 | class C: # CNL100 + 13 |+ +13 14 | async def foo(self): +14 15 | pass +15 16 | + + From a7b24237549c2ccc1b3c2878bfd8658c7a7ceab9 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 18 Feb 2024 18:22:13 -0500 Subject: [PATCH 6/9] add rule description --- .../rules/missing_class_newline.rs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs index 6c173276ef24d..adf263f13b4b6 100644 --- a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs +++ b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs @@ -11,7 +11,31 @@ use crate::line_width::IndentWidth; use crate::rules::pycodestyle::rules::{ LogicalLineInfo, LogicalLineKind, LinePreprocessor}; - +/// ## What it does +/// Checks for a missing blank line between a class definition and its first method. +/// +/// ## Why is this bad? +/// PEP8 says we should surround every class method with a single blank line. +/// However it is ambiguous about the first method having a blank line above it. +/// This rule enforces a single blank line, for consistency across classes. +/// +/// ## Example +/// ```python +/// class MyClass(object): +/// def func1(): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// class MyClass(object): +/// +/// def func1(): +/// pass +/// ``` +/// +/// ## References +/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines) #[violation] pub struct MissingClassNewLine; From ee3e433af5bc06fecc7d15c6f57b75da6e4cc10c Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 18 Feb 2024 18:29:23 -0500 Subject: [PATCH 7/9] update license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/LICENSE b/LICENSE index 04ed9285de85a..34604a2fefa1e 100644 --- a/LICENSE +++ b/LICENSE @@ -1371,3 +1371,24 @@ are: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + +- flake8-class-newline, licensed as follows: + """ + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ From 1939eeda162f673b396dd1865f7dc2b2af9fa304 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 18 Feb 2024 18:41:08 -0500 Subject: [PATCH 8/9] linting --- crates/ruff_linter/src/checkers/tokens.rs | 5 +++-- .../rules/missing_class_newline.rs | 22 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index 1a0e273bbd178..4e0c174a54f21 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -15,8 +15,9 @@ use crate::directives::TodoComment; use crate::registry::{AsRule, Rule}; use crate::rules::pycodestyle::rules::BlankLinesChecker; use crate::rules::{ - eradicate, flake8_class_newline, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat, - flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff, + eradicate, flake8_class_newline, flake8_commas, flake8_executable, flake8_fixme, + flake8_implicit_str_concat, flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, + pylint, pyupgrade, ruff, }; use crate::settings::LinterSettings; diff --git a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs index adf263f13b4b6..fe5b9f42ae7b8 100644 --- a/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs +++ b/crates/ruff_linter/src/rules/flake8_class_newline/rules/missing_class_newline.rs @@ -5,11 +5,10 @@ use ruff_diagnostics::Fix; use ruff_macros::{derive_message_formats, violation}; use ruff_python_codegen::Stylist; use ruff_python_parser::lexer::LexResult; -use ruff_source_file::{Locator}; +use ruff_source_file::Locator; use crate::line_width::IndentWidth; -use crate::rules::pycodestyle::rules::{ - LogicalLineInfo, LogicalLineKind, LinePreprocessor}; +use crate::rules::pycodestyle::rules::{LinePreprocessor, LogicalLineInfo, LogicalLineKind}; /// ## What it does /// Checks for a missing blank line between a class definition and its first method. @@ -50,7 +49,6 @@ impl AlwaysFixableViolation for MissingClassNewLine { } } - #[derive(Copy, Clone, Debug, Default)] enum Follows { #[default] @@ -58,7 +56,6 @@ enum Follows { Other, } - /// Contains variables used for the linting of blank lines. #[derive(Debug, Default)] pub(crate) struct BlankLinesChecker { @@ -81,7 +78,7 @@ impl BlankLinesChecker { &logical_line, locator, stylist, - diagnostics + diagnostics, ); } } @@ -93,11 +90,14 @@ impl BlankLinesChecker { stylist: &Stylist, diagnostics: &mut Vec, ) { - if (matches!(self.follows, Follows::Class) && matches!(line.kind, LogicalLineKind::Function | LogicalLineKind::Decorator) && line.preceding_blank_lines == 0) { - let mut diagnostic = Diagnostic::new( - MissingClassNewLine, - line.first_token_range - ); + if (matches!(self.follows, Follows::Class) + && matches!( + line.kind, + LogicalLineKind::Function | LogicalLineKind::Decorator + ) + && line.preceding_blank_lines == 0) + { + let mut diagnostic = Diagnostic::new(MissingClassNewLine, 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()), From 4282d154a272f3914ee1cd35814ed7351c0ad3fa Mon Sep 17 00:00:00 2001 From: augustelalande Date: Mon, 26 Feb 2024 22:21:52 -0500 Subject: [PATCH 9/9] exclude stub files from rule --- crates/ruff_linter/src/checkers/tokens.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index 4e0c174a54f21..6fa578d2dea57 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -210,7 +210,7 @@ pub(crate) fn check_tokens( flake8_fixme::rules::todos(&mut diagnostics, &todo_comments); } - if settings.rules.enabled(Rule::MissingClassNewLine) { + if !source_type.is_stub() && settings.rules.enabled(Rule::MissingClassNewLine) { let mut blank_lines_checker = flake8_class_newline::rules::BlankLinesChecker::default(); blank_lines_checker.check_lines( tokens,