[pycodestyle] Implement blank-line-at-end-of-file (W391) (#10243)

## Summary

Implements the [blank line at end of
file](https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes)
rule (W391) from pycodestyle. Renamed to TooManyNewlinesAtEndOfFile for
clarity.

## Test Plan

New fixtures have been added

Part of #2402
This commit is contained in:
Auguste Lalande 2024-03-11 22:07:59 -04:00 committed by GitHub
parent c746912b9e
commit b117f33075
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 232 additions and 1 deletions

2
.gitattributes vendored
View file

@ -2,6 +2,8 @@
crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf 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/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/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 crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf

View file

@ -0,0 +1,14 @@
# Unix style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View file

@ -0,0 +1,13 @@
# Unix style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View file

@ -0,0 +1,17 @@
# Windows style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View file

@ -0,0 +1,13 @@
# Windows style
def foo() -> None:
pass
def bar() -> None:
pass
if __name__ == '__main__':
foo()
bar()

View file

@ -0,0 +1,5 @@
# This is fine
def foo():
pass
# Some comment

View file

@ -203,6 +203,10 @@ pub(crate) fn check_tokens(
flake8_fixme::rules::todos(&mut diagnostics, &todo_comments); 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.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule()));
diagnostics diagnostics

View file

@ -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, "W291") => (RuleGroup::Stable, rules::pycodestyle::rules::TrailingWhitespace),
(Pycodestyle, "W292") => (RuleGroup::Stable, rules::pycodestyle::rules::MissingNewlineAtEndOfFile), (Pycodestyle, "W292") => (RuleGroup::Stable, rules::pycodestyle::rules::MissingNewlineAtEndOfFile),
(Pycodestyle, "W293") => (RuleGroup::Stable, rules::pycodestyle::rules::BlankLineWithWhitespace), (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, "W505") => (RuleGroup::Stable, rules::pycodestyle::rules::DocLineTooLong),
(Pycodestyle, "W605") => (RuleGroup::Stable, rules::pycodestyle::rules::InvalidEscapeSequence), (Pycodestyle, "W605") => (RuleGroup::Stable, rules::pycodestyle::rules::InvalidEscapeSequence),

View file

@ -300,6 +300,7 @@ impl Rule {
| Rule::SingleLineImplicitStringConcatenation | Rule::SingleLineImplicitStringConcatenation
| Rule::TabIndentation | Rule::TabIndentation
| Rule::TooManyBlankLines | Rule::TooManyBlankLines
| Rule::TooManyNewlinesAtEndOfFile
| Rule::TrailingCommaOnBareTuple | Rule::TrailingCommaOnBareTuple
| Rule::TypeCommentInStub | Rule::TypeCommentInStub
| Rule::UselessSemicolon | Rule::UselessSemicolon

View file

@ -72,6 +72,11 @@ mod tests {
#[test_case(Rule::TypeComparison, Path::new("E721.py"))] #[test_case(Rule::TypeComparison, Path::new("E721.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))] #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
#[test_case(Rule::RedundantBackslash, Path::new("E502.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<()> { fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!( let snapshot = format!(
"preview__{}_{}", "preview__{}_{}",

View file

@ -42,7 +42,7 @@ pub(crate) fn no_newline_at_end_of_file(
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {
let source = locator.contents(); let source = locator.contents();
// Ignore empty and BOM only files // Ignore empty and BOM only files.
if source.is_empty() || source == "\u{feff}" { if source.is_empty() || source == "\u{feff}" {
return None; return None;
} }

View file

@ -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 multiple_imports_on_one_line::*;
pub(crate) use not_tests::*; pub(crate) use not_tests::*;
pub(crate) use tab_indentation::*; pub(crate) use tab_indentation::*;
pub(crate) use too_many_newlines_at_end_of_file::*;
pub(crate) use trailing_whitespace::*; pub(crate) use trailing_whitespace::*;
pub(crate) use type_comparison::*; pub(crate) use type_comparison::*;
@ -39,5 +40,6 @@ mod module_import_not_at_top_of_file;
mod multiple_imports_on_one_line; mod multiple_imports_on_one_line;
mod not_tests; mod not_tests;
mod tab_indentation; mod tab_indentation;
mod too_many_newlines_at_end_of_file;
mod trailing_whitespace; mod trailing_whitespace;
mod type_comparison; mod type_comparison;

View file

@ -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<Diagnostic>,
lxr: &[LexResult],
) {
let mut num_trailing_newlines = 0u32;
let mut start: Option<TextSize> = None;
let mut end: Option<TextSize> = 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);
}

View file

@ -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 |-

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View file

@ -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 |-

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

3
ruff.schema.json generated
View file

@ -3816,6 +3816,9 @@
"W291", "W291",
"W292", "W292",
"W293", "W293",
"W3",
"W39",
"W391",
"W5", "W5",
"W50", "W50",
"W505", "W505",

View file

@ -105,6 +105,7 @@ KNOWN_PARSE_ERRORS = [
"tab-after-operator", "tab-after-operator",
"tab-before-keyword", "tab-before-keyword",
"tab-before-operator", "tab-before-operator",
"too-many-newlines-at-end-of-file",
"trailing-whitespace", "trailing-whitespace",
"unexpected-indentation", "unexpected-indentation",
] ]