From 1d3b602ac03a2831079977ff8b65a840160c2de5 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Tue, 5 Mar 2024 23:04:40 -0500 Subject: [PATCH 01/13] implement too_many_newlines_at_end_of_file --- .../test/fixtures/pycodestyle/W391_0.py | 15 ++++++ .../test/fixtures/pycodestyle/W391_1.py | 12 +++++ .../src/checkers/physical_lines.rs | 10 +++- crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/registry.rs | 1 + .../ruff_linter/src/rules/pycodestyle/mod.rs | 2 + .../src/rules/pycodestyle/rules/mod.rs | 2 + .../rules/too_many_newlines_at_end_of_file.rs | 52 +++++++++++++++++++ 8 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py create mode 100644 crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py new file mode 100644 index 0000000000000..52ca8bf5d9843 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py @@ -0,0 +1,15 @@ +def foo() -> None: + pass + + +def bar() -> None: + pass + + + +if __name__ == '__main__': + foo() + bar() + + + diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py new file mode 100644 index 0000000000000..434c565a18690 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py @@ -0,0 +1,12 @@ +def foo() -> None: + pass + + +def bar() -> None: + pass + + + +if __name__ == '__main__': + foo() + bar() diff --git a/crates/ruff_linter/src/checkers/physical_lines.rs b/crates/ruff_linter/src/checkers/physical_lines.rs index fbb9abff633e3..013208068f66b 100644 --- a/crates/ruff_linter/src/checkers/physical_lines.rs +++ b/crates/ruff_linter/src/checkers/physical_lines.rs @@ -10,7 +10,7 @@ use crate::registry::Rule; use crate::rules::flake8_copyright::rules::missing_copyright_notice; use crate::rules::pycodestyle::rules::{ doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file, - trailing_whitespace, + too_many_newlines_at_end_of_file, trailing_whitespace, }; use crate::rules::pylint; use crate::settings::LinterSettings; @@ -27,6 +27,8 @@ pub(crate) fn check_physical_lines( let enforce_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong); let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong); let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile); + let enforce_too_many_newlines_at_end_of_file = + settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile); let enforce_mixed_spaces_and_tabs = settings.rules.enabled(Rule::MixedSpacesAndTabs); let enforce_bidirectional_unicode = settings.rules.enabled(Rule::BidirectionalUnicode); let enforce_trailing_whitespace = settings.rules.enabled(Rule::TrailingWhitespace); @@ -77,6 +79,12 @@ pub(crate) fn check_physical_lines( } } + if enforce_too_many_newlines_at_end_of_file { + if let Some(diagnostic) = too_many_newlines_at_end_of_file(locator) { + diagnostics.push(diagnostic); + } + } + if enforce_copyright_notice { if let Some(diagnostic) = missing_copyright_notice(locator, settings) { diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 5cf0293d1292e..61e0dfefe6e93 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -167,6 +167,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pycodestyle, "W291") => (RuleGroup::Stable, rules::pycodestyle::rules::TrailingWhitespace), (Pycodestyle, "W292") => (RuleGroup::Stable, rules::pycodestyle::rules::MissingNewlineAtEndOfFile), (Pycodestyle, "W293") => (RuleGroup::Stable, rules::pycodestyle::rules::BlankLineWithWhitespace), + (Pycodestyle, "W391") => (RuleGroup::Preview, rules::pycodestyle::rules::TooManyNewlinesAtEndOfFile), (Pycodestyle, "W505") => (RuleGroup::Stable, rules::pycodestyle::rules::DocLineTooLong), (Pycodestyle, "W605") => (RuleGroup::Stable, rules::pycodestyle::rules::InvalidEscapeSequence), diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index e85f14d7116af..3af6fa8b7ca3c 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -254,6 +254,7 @@ impl Rule { | Rule::MissingCopyrightNotice | Rule::MissingNewlineAtEndOfFile | Rule::MixedSpacesAndTabs + | Rule::TooManyNewlinesAtEndOfFile | Rule::TrailingWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AvoidableEscapedQuote diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 34b63a26ffbe9..d83d12d1e139d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -71,6 +71,8 @@ mod tests { #[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))] #[test_case(Rule::TypeComparison, Path::new("E721.py"))] #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))] + #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_0.py"))] + #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_1.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs index 686b6bdc2c5b6..178dd13b5be43 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs @@ -17,6 +17,7 @@ pub(crate) use module_import_not_at_top_of_file::*; pub(crate) use multiple_imports_on_one_line::*; pub(crate) use not_tests::*; pub(crate) use tab_indentation::*; +pub(crate) use too_many_newlines_at_end_of_file::*; pub(crate) use trailing_whitespace::*; pub(crate) use type_comparison::*; @@ -39,5 +40,6 @@ mod module_import_not_at_top_of_file; mod multiple_imports_on_one_line; mod not_tests; mod tab_indentation; +mod too_many_newlines_at_end_of_file; mod trailing_whitespace; mod type_comparison; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs new file mode 100644 index 0000000000000..21e7bd4d66f3f --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -0,0 +1,52 @@ +use ruff_text_size::{TextLen, TextRange}; + +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_codegen::Stylist; +use ruff_source_file::Locator; + +#[violation] +pub struct TooManyNewlinesAtEndOfFile; + +impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { + #[derive_message_formats] + fn message(&self) -> String { + "Too many newlines at the end of file".to_string() + } + + fn fix_title(&self) -> String { + "Remove extraneous trailing newlines".to_string() + } +} + +/// W391 +pub(crate) fn too_many_newlines_at_end_of_file( + locator: &Locator, +) -> Option { + let source = locator.contents(); + + // Ignore empty and BOM only files + if source.is_empty() || source == "\u{feff}" { + return None; + } + + // Regex to match multiple newline characters at the end of the file + let newline_regex = Regex::new(r"(\n|\r\n){2,}$").unwrap(); + + if let Some(mat) = newline_regex.find(source) { + let start_pos = mat.start(); + let end_pos = mat.end(); + + // Calculate the TextRange to keep only one newline at the end + let range = TextRange::new( + TextLen::from(start_pos as u32 + 1), // Keep one newline + TextLen::from(end_pos as u32), + ); + + let mut diagnostic = Diagnostic::new(TooManyNewlinesAtEndOfFile, range); + diagnostic.set_fix(Fix::safe_edit(Edit::deletion(range))); + return Some(diagnostic); + } + + None +} From 065f6435aa211194b35f7de39ab12ad59bd8e9f6 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Tue, 5 Mar 2024 23:09:44 -0500 Subject: [PATCH 02/13] add rule description --- .../rules/too_many_newlines_at_end_of_file.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 21e7bd4d66f3f..6b4c6a21a6f97 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -5,6 +5,22 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_codegen::Stylist; use ruff_source_file::Locator; +/// ## What it does +/// Checks for files with too many new lines at the end of the file. +/// +/// ## Why is this bad? +/// Trailing blank lines are superfluous. +/// However the last line should end with a new line. +/// +/// ## Example +/// ```python +/// spam(1)\n\n\n +/// ``` +/// +/// Use instead: +/// ```python +/// spam(1)\n +/// ``` #[violation] pub struct TooManyNewlinesAtEndOfFile; @@ -31,7 +47,7 @@ pub(crate) fn too_many_newlines_at_end_of_file( } // Regex to match multiple newline characters at the end of the file - let newline_regex = Regex::new(r"(\n|\r\n){2,}$").unwrap(); + let newline_regex = Regex::new(r"(\n|\r){2,}$").unwrap(); if let Some(mat) = newline_regex.find(source) { let start_pos = mat.start(); From fed92fd6fe0bb02ba1abeac9a51ebe062f387d16 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Tue, 5 Mar 2024 23:15:35 -0500 Subject: [PATCH 03/13] add test cases with different lines endings --- .gitattributes | 4 ++++ .../test/fixtures/pycodestyle/W391_0.py | 1 + .../test/fixtures/pycodestyle/W391_1.py | 1 + .../test/fixtures/pycodestyle/W391_2.py | 13 +++++++++++++ .../test/fixtures/pycodestyle/W391_3.py | 16 ++++++++++++++++ .../test/fixtures/pycodestyle/W391_4.py | 1 + .../test/fixtures/pycodestyle/W391_5.py | 1 + 7 files changed, 37 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py diff --git a/.gitattributes b/.gitattributes index 8dd4fe466ad6e..28fc8577d1af8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,10 @@ crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf +crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf +crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf +crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py text eol=cr +crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py text eol=cr crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py index 52ca8bf5d9843..5348f871870e6 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py @@ -1,3 +1,4 @@ +# Unix style def foo() -> None: pass diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py index 434c565a18690..3264a5eedffd3 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py @@ -1,3 +1,4 @@ +# Unix style def foo() -> None: pass diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py new file mode 100644 index 0000000000000..151b1a248c1de --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py @@ -0,0 +1,13 @@ +# Windows style +def foo() -> None: + pass + + +def bar() -> None: + pass + + + +if __name__ == '__main__': + foo() + bar() diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py new file mode 100644 index 0000000000000..5bba0e3f15638 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py @@ -0,0 +1,16 @@ +# Windows style +def foo() -> None: + pass + + +def bar() -> None: + pass + + + +if __name__ == '__main__': + foo() + bar() + + + diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py new file mode 100644 index 0000000000000..220bb5ad4c393 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py @@ -0,0 +1 @@ +# Mac OS style def foo() -> None: pass def bar() -> None: pass if __name__ == '__main__': foo() bar() \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py new file mode 100644 index 0000000000000..819fc5669f8ee --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py @@ -0,0 +1 @@ +# Mac OS style def foo() -> None: pass def bar() -> None: pass if __name__ == '__main__': foo() bar() \ No newline at end of file From 82adae1b6f40f26d9cc7f676bc9e23b073fbb50f Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 00:30:58 -0500 Subject: [PATCH 04/13] run tests --- .gitattributes | 2 -- .../test/fixtures/pycodestyle/W391_2.py | 4 +++ .../test/fixtures/pycodestyle/W391_3.py | 3 --- .../test/fixtures/pycodestyle/W391_4.py | 1 - .../test/fixtures/pycodestyle/W391_5.py | 1 - .../ruff_linter/src/rules/pycodestyle/mod.rs | 2 ++ .../rules/too_many_newlines_at_end_of_file.rs | 21 +++++++--------- ...style__tests__preview__W391_W391_0.py.snap | 20 +++++++++++++++ ...style__tests__preview__W391_W391_1.py.snap | 4 +++ ...style__tests__preview__W391_W391_2.py.snap | 25 +++++++++++++++++++ ...style__tests__preview__W391_W391_3.py.snap | 4 +++ ruff.schema.json | 3 +++ 12 files changed, 71 insertions(+), 19 deletions(-) delete mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py delete mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_1.py.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_3.py.snap diff --git a/.gitattributes b/.gitattributes index 28fc8577d1af8..d7d5267dea592 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,8 +4,6 @@ crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=cr crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf -crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py text eol=cr -crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py text eol=cr crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py index 151b1a248c1de..a45c7a1cf0b66 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py @@ -11,3 +11,7 @@ def bar() -> None: if __name__ == '__main__': foo() bar() + + + + diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py index 5bba0e3f15638..151b1a248c1de 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py @@ -11,6 +11,3 @@ def bar() -> None: if __name__ == '__main__': foo() bar() - - - diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py deleted file mode 100644 index 220bb5ad4c393..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py +++ /dev/null @@ -1 +0,0 @@ -# Mac OS style def foo() -> None: pass def bar() -> None: pass if __name__ == '__main__': foo() bar() \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py deleted file mode 100644 index 819fc5669f8ee..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_5.py +++ /dev/null @@ -1 +0,0 @@ -# Mac OS style def foo() -> None: pass def bar() -> None: pass if __name__ == '__main__': foo() bar() \ No newline at end of file diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index d83d12d1e139d..651aa1ec6ec06 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -73,6 +73,8 @@ mod tests { #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_0.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_1.py"))] + #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_2.py"))] + #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_3.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 6b4c6a21a6f97..7de0e1f8440f7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -1,8 +1,9 @@ -use ruff_text_size::{TextLen, TextRange}; +use regex::Regex; + +use ruff_text_size::{TextSize, TextRange}; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_codegen::Stylist; use ruff_source_file::Locator; /// ## What it does @@ -27,7 +28,7 @@ pub struct TooManyNewlinesAtEndOfFile; impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { #[derive_message_formats] fn message(&self) -> String { - "Too many newlines at the end of file".to_string() + format!("Too many newlines at the end of file") } fn fix_title(&self) -> String { @@ -47,20 +48,16 @@ pub(crate) fn too_many_newlines_at_end_of_file( } // Regex to match multiple newline characters at the end of the file - let newline_regex = Regex::new(r"(\n|\r){2,}$").unwrap(); + let newline_regex = Regex::new(r"(\r\n){2,}$|\n{2,}$|\r{2,}$").unwrap(); if let Some(mat) = newline_regex.find(source) { - let start_pos = mat.start(); - let end_pos = mat.end(); + let start_pos = TextSize::new(mat.start() as u32 + 1); + let end_pos = TextSize::new(mat.end() as u32); // Calculate the TextRange to keep only one newline at the end - let range = TextRange::new( - TextLen::from(start_pos as u32 + 1), // Keep one newline - TextLen::from(end_pos as u32), - ); - + let range = TextRange::new(start_pos, end_pos); let mut diagnostic = Diagnostic::new(TooManyNewlinesAtEndOfFile, range); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion(range))); + diagnostic.set_fix(Fix::safe_edit(Edit::deletion(start_pos, end_pos))); return Some(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap new file mode 100644 index 0000000000000..3e9f353894eb4 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +W391_0.py:14:1: W391 [*] Too many newlines at the end of file + | +12 | foo() +13 | bar() +14 | / +15 | | +16 | | + | + = help: Remove extraneous trailing newlines + +ℹ Safe fix +11 11 | if __name__ == '__main__': +12 12 | foo() +13 13 | bar() +14 |- +15 |- +16 |- diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_1.py.snap new file mode 100644 index 0000000000000..6dcc4546f11f9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_1.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap new file mode 100644 index 0000000000000..2f406b3055c20 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +W391_2.py:13:11: W391 [*] Too many newlines at the end of file + | +11 | if __name__ == '__main__': +12 | foo() +13 | bar() +14 | | +15 | | +16 | | +17 | | + | + = help: Remove extraneous trailing newlines + +ℹ Safe fix +10 10 | +11 11 | if __name__ == '__main__': +12 12 | foo() +13 |- bar() +14 |- +15 |- +16 |- +17 |- + 13 |+ bar() diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_3.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_3.py.snap new file mode 100644 index 0000000000000..6dcc4546f11f9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_3.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + diff --git a/ruff.schema.json b/ruff.schema.json index 674003ed05df2..598be111c5331 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3797,6 +3797,9 @@ "W291", "W292", "W293", + "W3", + "W39", + "W391", "W5", "W50", "W505", From 5ba8358a1b40867f10bdf547da1ae81c16146ceb Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 00:38:07 -0500 Subject: [PATCH 05/13] linting --- .../pycodestyle/rules/too_many_newlines_at_end_of_file.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 7de0e1f8440f7..7e7a483023f91 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -1,6 +1,6 @@ use regex::Regex; -use ruff_text_size::{TextSize, TextRange}; +use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -37,9 +37,7 @@ impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { } /// W391 -pub(crate) fn too_many_newlines_at_end_of_file( - locator: &Locator, -) -> Option { +pub(crate) fn too_many_newlines_at_end_of_file(locator: &Locator) -> Option { let source = locator.contents(); // Ignore empty and BOM only files From 67753eb53d5ac5852018860e988e7753b76991a2 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 00:58:22 -0500 Subject: [PATCH 06/13] address clippy issue --- .../pycodestyle/rules/too_many_newlines_at_end_of_file.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 7e7a483023f91..6cecbce670efd 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -49,8 +49,8 @@ pub(crate) fn too_many_newlines_at_end_of_file(locator: &Locator) -> Option Date: Wed, 6 Mar 2024 01:05:46 -0500 Subject: [PATCH 07/13] seems to be a KNOWN_PARSE_ERRORS --- scripts/check_docs_formatted.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 234fb825e67a9..f8744781ad150 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -104,6 +104,7 @@ "tab-after-operator", "tab-before-keyword", "tab-before-operator", + "too-many-newlines-at-end-of-file", "trailing-whitespace", "unexpected-indentation", ] From 7ff02c69f855f4e9e524703bf5e55147171935f6 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 14:21:48 -0500 Subject: [PATCH 08/13] Change W391 to be token based --- .../src/checkers/physical_lines.rs | 10 +--- crates/ruff_linter/src/checkers/tokens.rs | 4 ++ crates/ruff_linter/src/registry.rs | 2 +- .../rules/too_many_newlines_at_end_of_file.rs | 50 ++++++++++++------- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/crates/ruff_linter/src/checkers/physical_lines.rs b/crates/ruff_linter/src/checkers/physical_lines.rs index 013208068f66b..fbb9abff633e3 100644 --- a/crates/ruff_linter/src/checkers/physical_lines.rs +++ b/crates/ruff_linter/src/checkers/physical_lines.rs @@ -10,7 +10,7 @@ use crate::registry::Rule; use crate::rules::flake8_copyright::rules::missing_copyright_notice; use crate::rules::pycodestyle::rules::{ doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file, - too_many_newlines_at_end_of_file, trailing_whitespace, + trailing_whitespace, }; use crate::rules::pylint; use crate::settings::LinterSettings; @@ -27,8 +27,6 @@ pub(crate) fn check_physical_lines( let enforce_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong); let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong); let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile); - let enforce_too_many_newlines_at_end_of_file = - settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile); let enforce_mixed_spaces_and_tabs = settings.rules.enabled(Rule::MixedSpacesAndTabs); let enforce_bidirectional_unicode = settings.rules.enabled(Rule::BidirectionalUnicode); let enforce_trailing_whitespace = settings.rules.enabled(Rule::TrailingWhitespace); @@ -79,12 +77,6 @@ pub(crate) fn check_physical_lines( } } - if enforce_too_many_newlines_at_end_of_file { - if let Some(diagnostic) = too_many_newlines_at_end_of_file(locator) { - diagnostics.push(diagnostic); - } - } - if enforce_copyright_notice { if let Some(diagnostic) = missing_copyright_notice(locator, settings) { diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index c676645815669..b21a814c419d9 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -209,6 +209,10 @@ pub(crate) fn check_tokens( flake8_fixme::rules::todos(&mut diagnostics, &todo_comments); } + if settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile) { + pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens, locator); + } + diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())); diagnostics diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 3af6fa8b7ca3c..fa84b36368be1 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -254,7 +254,6 @@ impl Rule { | Rule::MissingCopyrightNotice | Rule::MissingNewlineAtEndOfFile | Rule::MixedSpacesAndTabs - | Rule::TooManyNewlinesAtEndOfFile | Rule::TrailingWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AvoidableEscapedQuote @@ -301,6 +300,7 @@ impl Rule { | Rule::SingleLineImplicitStringConcatenation | Rule::TabIndentation | Rule::TooManyBlankLines + | Rule::TooManyNewlinesAtEndOfFile | Rule::TrailingCommaOnBareTuple | Rule::TypeCommentInStub | Rule::UselessSemicolon diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 6cecbce670efd..8f2a0a930e826 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -1,10 +1,9 @@ -use regex::Regex; - -use ruff_text_size::{TextRange, TextSize}; - use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_parser::lexer::LexResult; +use ruff_python_parser::Tok; use ruff_source_file::Locator; +use ruff_text_size::{TextRange, TextSize}; /// ## What it does /// Checks for files with too many new lines at the end of the file. @@ -37,27 +36,44 @@ impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { } /// W391 -pub(crate) fn too_many_newlines_at_end_of_file(locator: &Locator) -> Option { +pub(crate) fn too_many_newlines_at_end_of_file( + diagnostics: &mut Vec, + lxr: &[LexResult], + locator: &Locator +) { let source = locator.contents(); // Ignore empty and BOM only files if source.is_empty() || source == "\u{feff}" { - return None; + return; } - // Regex to match multiple newline characters at the end of the file - let newline_regex = Regex::new(r"(\r\n){2,}$|\n{2,}$|\r{2,}$").unwrap(); + let mut count = 0; + let mut start_pos: Option = None; + let mut end_pos: Option = None; - if let Some(mat) = newline_regex.find(source) { - let start_pos = TextSize::new((mat.start() + 1).try_into().unwrap()); - let end_pos = TextSize::new(mat.end().try_into().unwrap()); + for &(ref tok, range) in lxr.iter().rev().flatten() { + match tok { + Tok::NonLogicalNewline | Tok::Newline => { + if count == 0 { + end_pos = Some(range.end()); + } + count += 1; + } + Tok::Dedent => continue, + _ => { + start_pos = Some(range.end()); + break; + } + } + } - // Calculate the TextRange to keep only one newline at the end - let range = TextRange::new(start_pos, end_pos); + if count > 1 { + let start = start_pos.unwrap() + TextSize::from(1); + let end = end_pos.unwrap(); + let range = TextRange::new(start, end); let mut diagnostic = Diagnostic::new(TooManyNewlinesAtEndOfFile, range); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion(start_pos, end_pos))); - return Some(diagnostic); + diagnostic.set_fix(Fix::safe_edit(Edit::deletion(start, end))); + diagnostics.push(diagnostic); } - - None } From 60d4ce5b062aecec64ff4f606380cb144f60a70e Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 14:23:17 -0500 Subject: [PATCH 09/13] linting --- .../rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 8f2a0a930e826..050d8afc7e205 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -39,7 +39,7 @@ impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { pub(crate) fn too_many_newlines_at_end_of_file( diagnostics: &mut Vec, lxr: &[LexResult], - locator: &Locator + locator: &Locator, ) { let source = locator.contents(); From 6e2dc197c4701745c6bde48d7d8cf3e1a383a2f4 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 18:25:01 -0500 Subject: [PATCH 10/13] fix bug with windows style newline --- .../rules/too_many_newlines_at_end_of_file.rs | 4 ++-- ...les__pycodestyle__tests__preview__W391_W391_2.py.snap | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 050d8afc7e205..7cea78d8bb704 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -54,7 +54,7 @@ pub(crate) fn too_many_newlines_at_end_of_file( for &(ref tok, range) in lxr.iter().rev().flatten() { match tok { - Tok::NonLogicalNewline | Tok::Newline => { + Tok::NonLogicalNewline => { if count == 0 { end_pos = Some(range.end()); } @@ -69,7 +69,7 @@ pub(crate) fn too_many_newlines_at_end_of_file( } if count > 1 { - let start = start_pos.unwrap() + TextSize::from(1); + let start = start_pos.unwrap(); let end = end_pos.unwrap(); let range = TextRange::new(start, end); let mut diagnostic = Diagnostic::new(TooManyNewlinesAtEndOfFile, range); diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap index 2f406b3055c20..fa84d6ad4f412 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap @@ -1,12 +1,11 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -W391_2.py:13:11: W391 [*] Too many newlines at the end of file +W391_2.py:14:1: W391 [*] Too many newlines at the end of file | -11 | if __name__ == '__main__': 12 | foo() 13 | bar() -14 | | +14 | / 15 | | 16 | | 17 | | @@ -14,12 +13,10 @@ W391_2.py:13:11: W391 [*] Too many newlines at the end of file = help: Remove extraneous trailing newlines ℹ Safe fix -10 10 | 11 11 | if __name__ == '__main__': 12 12 | foo() -13 |- bar() +13 13 | bar() 14 |- 15 |- 16 |- 17 |- - 13 |+ bar() From ef3c813ea869fd814213419506a6c9ed35b5503e Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 18:33:04 -0500 Subject: [PATCH 11/13] fix bug not detecting single newline --- .../resources/test/fixtures/pycodestyle/W391_0.py | 2 -- .../rules/too_many_newlines_at_end_of_file.rs | 2 +- ...__pycodestyle__tests__preview__W391_W391_0.py.snap | 11 ++++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py index 5348f871870e6..1fa6e8e931b8a 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py @@ -12,5 +12,3 @@ def bar() -> None: foo() bar() - - diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 7cea78d8bb704..ddead5696f9db 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -68,7 +68,7 @@ pub(crate) fn too_many_newlines_at_end_of_file( } } - if count > 1 { + if count >= 1 { let start = start_pos.unwrap(); let end = end_pos.unwrap(); let range = TextRange::new(start, end); diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap index 3e9f353894eb4..21c2b4ad04a9e 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap @@ -3,11 +3,10 @@ source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- W391_0.py:14:1: W391 [*] Too many newlines at the end of file | -12 | foo() -13 | bar() -14 | / -15 | | -16 | | +12 | foo() +13 | bar() +14 | + | ^ W391 | = help: Remove extraneous trailing newlines @@ -16,5 +15,3 @@ W391_0.py:14:1: W391 [*] Too many newlines at the end of file 12 12 | foo() 13 13 | bar() 14 |- -15 |- -16 |- From 815087a001173ac5bb0cecbb1b1de8af7900d93c Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 6 Mar 2024 19:19:25 -0500 Subject: [PATCH 12/13] fix bug with files ending in comments --- .../resources/test/fixtures/pycodestyle/W391_4.py | 5 +++++ crates/ruff_linter/src/rules/pycodestyle/mod.rs | 1 + .../pycodestyle/rules/too_many_newlines_at_end_of_file.rs | 6 +++--- ..._rules__pycodestyle__tests__preview__W391_W391_4.py.snap | 4 ++++ 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_4.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py new file mode 100644 index 0000000000000..4407beda73301 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py @@ -0,0 +1,5 @@ +# This is fine +def foo(): + pass + + # Some comment diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 651aa1ec6ec06..6982935648054 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -75,6 +75,7 @@ mod tests { #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_1.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_2.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_3.py"))] + #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_4.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index ddead5696f9db..9f58066c180c5 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -54,21 +54,21 @@ pub(crate) fn too_many_newlines_at_end_of_file( for &(ref tok, range) in lxr.iter().rev().flatten() { match tok { - Tok::NonLogicalNewline => { + Tok::NonLogicalNewline | Tok::Newline => { if count == 0 { end_pos = Some(range.end()); } + start_pos = Some(range.end()); count += 1; } Tok::Dedent => continue, _ => { - start_pos = Some(range.end()); break; } } } - if count >= 1 { + if count > 1 { let start = start_pos.unwrap(); let end = end_pos.unwrap(); let range = TextRange::new(start, end); diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_4.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_4.py.snap new file mode 100644 index 0000000000000..6dcc4546f11f9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_4.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- + From 2d4e2fefb9bc971e0a1ff5890c7b9097a72d7135 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 11 Mar 2024 21:34:40 -0400 Subject: [PATCH 13/13] Tweak message; remove locator --- crates/ruff_linter/src/checkers/tokens.rs | 2 +- .../rules/missing_newline_at_end_of_file.rs | 2 +- .../rules/too_many_newlines_at_end_of_file.rs | 80 ++++++++++++------- ...style__tests__preview__W391_W391_0.py.snap | 4 +- ...style__tests__preview__W391_W391_2.py.snap | 4 +- 5 files changed, 56 insertions(+), 36 deletions(-) diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index e3456e7149249..fa801a3284c74 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -204,7 +204,7 @@ pub(crate) fn check_tokens( } if settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile) { - pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens, locator); + pycodestyle::rules::too_many_newlines_at_end_of_file(&mut diagnostics, tokens); } diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())); diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs index 4713d0f35fd5a..f7ee6ae490123 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs @@ -42,7 +42,7 @@ pub(crate) fn no_newline_at_end_of_file( ) -> Option { let source = locator.contents(); - // Ignore empty and BOM only files + // Ignore empty and BOM only files. if source.is_empty() || source == "\u{feff}" { return None; } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index 9f58066c180c5..ec28e01b4ea28 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -2,15 +2,15 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_parser::lexer::LexResult; use ruff_python_parser::Tok; -use ruff_source_file::Locator; use ruff_text_size::{TextRange, TextSize}; /// ## What it does -/// Checks for files with too many new lines at the end of the file. +/// Checks for files with multiple trailing blank lines. /// /// ## Why is this bad? -/// Trailing blank lines are superfluous. -/// However the last line should end with a new line. +/// Trailing blank lines in a file are superfluous. +/// +/// However, the last line of the file should end with a newline. /// /// ## Example /// ```python @@ -22,16 +22,35 @@ use ruff_text_size::{TextRange, TextSize}; /// spam(1)\n /// ``` #[violation] -pub struct TooManyNewlinesAtEndOfFile; +pub struct TooManyNewlinesAtEndOfFile { + num_trailing_newlines: u32, +} impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { #[derive_message_formats] fn message(&self) -> String { - format!("Too many newlines at the end of file") + let TooManyNewlinesAtEndOfFile { + num_trailing_newlines, + } = self; + + // We expect a single trailing newline; so two trailing newlines is one too many, three + // trailing newlines is two too many, etc. + if *num_trailing_newlines > 2 { + format!("Too many newlines at end of file") + } else { + format!("Extra newline at end of file") + } } fn fix_title(&self) -> String { - "Remove extraneous trailing newlines".to_string() + let TooManyNewlinesAtEndOfFile { + num_trailing_newlines, + } = self; + if *num_trailing_newlines > 2 { + "Remove trailing newlines".to_string() + } else { + "Remove trailing newline".to_string() + } } } @@ -39,27 +58,20 @@ impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { pub(crate) fn too_many_newlines_at_end_of_file( diagnostics: &mut Vec, lxr: &[LexResult], - locator: &Locator, ) { - let source = locator.contents(); + let mut num_trailing_newlines = 0u32; + let mut start: Option = None; + let mut end: Option = None; - // Ignore empty and BOM only files - if source.is_empty() || source == "\u{feff}" { - return; - } - - let mut count = 0; - let mut start_pos: Option = None; - let mut end_pos: Option = None; - - for &(ref tok, range) in lxr.iter().rev().flatten() { + // Count the number of trailing newlines. + for (tok, range) in lxr.iter().rev().flatten() { match tok { Tok::NonLogicalNewline | Tok::Newline => { - if count == 0 { - end_pos = Some(range.end()); + if num_trailing_newlines == 0 { + end = Some(range.end()); } - start_pos = Some(range.end()); - count += 1; + start = Some(range.end()); + num_trailing_newlines += 1; } Tok::Dedent => continue, _ => { @@ -68,12 +80,20 @@ pub(crate) fn too_many_newlines_at_end_of_file( } } - if count > 1 { - let start = start_pos.unwrap(); - let end = end_pos.unwrap(); - let range = TextRange::new(start, end); - let mut diagnostic = Diagnostic::new(TooManyNewlinesAtEndOfFile, range); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion(start, end))); - diagnostics.push(diagnostic); + if num_trailing_newlines == 0 || num_trailing_newlines == 1 { + return; } + + let range = match (start, end) { + (Some(start), Some(end)) => TextRange::new(start, end), + _ => return, + }; + let mut diagnostic = Diagnostic::new( + TooManyNewlinesAtEndOfFile { + num_trailing_newlines, + }, + range, + ); + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); + diagnostics.push(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap index 21c2b4ad04a9e..643743f66be96 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap @@ -1,14 +1,14 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -W391_0.py:14:1: W391 [*] Too many newlines at the end of file +W391_0.py:14:1: W391 [*] Extra newline at end of file | 12 | foo() 13 | bar() 14 | | ^ W391 | - = help: Remove extraneous trailing newlines + = help: Remove trailing newline ℹ Safe fix 11 11 | if __name__ == '__main__': diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap index fa84d6ad4f412..8ca9ecd0c1458 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -W391_2.py:14:1: W391 [*] Too many newlines at the end of file +W391_2.py:14:1: W391 [*] Too many newlines at end of file | 12 | foo() 13 | bar() @@ -10,7 +10,7 @@ W391_2.py:14:1: W391 [*] Too many newlines at the end of file 16 | | 17 | | | - = help: Remove extraneous trailing newlines + = help: Remove trailing newlines ℹ Safe fix 11 11 | if __name__ == '__main__':