mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
[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:
parent
c746912b9e
commit
b117f33075
20 changed files with 232 additions and 1 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -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
|
||||||
|
|
14
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py
vendored
Normal file
14
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Unix style
|
||||||
|
def foo() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def bar() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
foo()
|
||||||
|
bar()
|
||||||
|
|
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py
vendored
Normal file
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_1.py
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Unix style
|
||||||
|
def foo() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def bar() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
foo()
|
||||||
|
bar()
|
17
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py
vendored
Normal file
17
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Windows style
|
||||||
|
def foo() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def bar() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
foo()
|
||||||
|
bar()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py
vendored
Normal file
13
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Windows style
|
||||||
|
def foo() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def bar() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
foo()
|
||||||
|
bar()
|
5
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_4.py
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# This is fine
|
||||||
|
def foo():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Some comment
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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__{}_{}",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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 |-
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||||
|
---
|
||||||
|
|
|
@ -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 |-
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||||
|
---
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||||
|
---
|
||||||
|
|
3
ruff.schema.json
generated
3
ruff.schema.json
generated
|
@ -3816,6 +3816,9 @@
|
||||||
"W291",
|
"W291",
|
||||||
"W292",
|
"W292",
|
||||||
"W293",
|
"W293",
|
||||||
|
"W3",
|
||||||
|
"W39",
|
||||||
|
"W391",
|
||||||
"W5",
|
"W5",
|
||||||
"W50",
|
"W50",
|
||||||
"W505",
|
"W505",
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue