diff --git a/.gitattributes b/.gitattributes index 8dd4fe466ad6e..d7d5267dea592 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,8 @@ 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_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 new file mode 100644 index 0000000000000..1fa6e8e931b8a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py @@ -0,0 +1,14 @@ +# Unix 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_1.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py new file mode 100644 index 0000000000000..3264a5eedffd3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py @@ -0,0 +1,13 @@ +# Unix 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_2.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py new file mode 100644 index 0000000000000..a45c7a1cf0b66 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py @@ -0,0 +1,17 @@ +# 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..151b1a248c1de --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.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_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/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index 762f4cc463cc1..fa801a3284c74 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -203,6 +203,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); + } + 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 3c865f9dc1063..ef05dcb053128 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -168,6 +168,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 1ae0df64de0eb..7f07227fc386f 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -300,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/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index e65258eb00fca..035e16e2b4fb5 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -72,6 +72,11 @@ mod tests { #[test_case(Rule::TypeComparison, Path::new("E721.py"))] #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))] #[test_case(Rule::RedundantBackslash, Path::new("E502.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"))] + #[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/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/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..ec28e01b4ea28 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -0,0 +1,99 @@ +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_text_size::{TextRange, TextSize}; + +/// ## What it does +/// Checks for files with multiple trailing blank lines. +/// +/// ## Why is this bad? +/// Trailing blank lines in a file are superfluous. +/// +/// However, the last line of the file should end with a newline. +/// +/// ## Example +/// ```python +/// spam(1)\n\n\n +/// ``` +/// +/// Use instead: +/// ```python +/// spam(1)\n +/// ``` +#[violation] +pub struct TooManyNewlinesAtEndOfFile { + num_trailing_newlines: u32, +} + +impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { + #[derive_message_formats] + fn message(&self) -> String { + 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 { + let TooManyNewlinesAtEndOfFile { + num_trailing_newlines, + } = self; + if *num_trailing_newlines > 2 { + "Remove trailing newlines".to_string() + } else { + "Remove trailing newline".to_string() + } + } +} + +/// W391 +pub(crate) fn too_many_newlines_at_end_of_file( + diagnostics: &mut Vec, + lxr: &[LexResult], +) { + let mut num_trailing_newlines = 0u32; + let mut start: Option = None; + let mut end: Option = None; + + // Count the number of trailing newlines. + for (tok, range) in lxr.iter().rev().flatten() { + match tok { + Tok::NonLogicalNewline | Tok::Newline => { + if num_trailing_newlines == 0 { + end = Some(range.end()); + } + start = Some(range.end()); + num_trailing_newlines += 1; + } + Tok::Dedent => continue, + _ => { + break; + } + } + } + + 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 new file mode 100644 index 0000000000000..643743f66be96 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_0.py.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +W391_0.py:14:1: W391 [*] Extra newline at end of file + | +12 | foo() +13 | bar() +14 | + | ^ W391 + | + = help: Remove trailing newline + +ℹ Safe fix +11 11 | if __name__ == '__main__': +12 12 | foo() +13 13 | bar() +14 |- 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..8ca9ecd0c1458 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__W391_W391_2.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +W391_2.py:14:1: W391 [*] Too many newlines at end of file + | +12 | foo() +13 | bar() +14 | / +15 | | +16 | | +17 | | + | + = help: Remove trailing newlines + +ℹ Safe fix +11 11 | if __name__ == '__main__': +12 12 | foo() +13 13 | bar() +14 |- +15 |- +16 |- +17 |- 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/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 +--- + diff --git a/ruff.schema.json b/ruff.schema.json index 20fffce9bc4d8..962fd86b5f5b6 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3816,6 +3816,9 @@ "W291", "W292", "W293", + "W3", + "W39", + "W391", "W5", "W50", "W505", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index cfb039093237c..c25e3d1929ca2 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -105,6 +105,7 @@ "tab-after-operator", "tab-before-keyword", "tab-before-operator", + "too-many-newlines-at-end-of-file", "trailing-whitespace", "unexpected-indentation", ]