Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 14 commits into from Mar 12, 2024
2 changes: 2 additions & 0 deletions .gitattributes
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions 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()

13 changes: 13 additions & 0 deletions 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()
17 changes: 17 additions & 0 deletions 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()




13 changes: 13 additions & 0 deletions 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()
@@ -0,0 +1,5 @@
# This is fine
def foo():
pass

# Some comment
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/checkers/tokens.rs
Expand Up @@ -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, locator);
}

diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule()));

diagnostics
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Expand Up @@ -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),

Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/registry.rs
Expand Up @@ -300,6 +300,7 @@ impl Rule {
| Rule::SingleLineImplicitStringConcatenation
| Rule::TabIndentation
| Rule::TooManyBlankLines
| Rule::TooManyNewlinesAtEndOfFile
| Rule::TrailingCommaOnBareTuple
| Rule::TypeCommentInStub
| Rule::UselessSemicolon
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff_linter/src/rules/pycodestyle/mod.rs
Expand Up @@ -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__{}_{}",
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs
Expand Up @@ -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::*;

Expand All @@ -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;
@@ -0,0 +1,79 @@
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.
///
/// ## 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;

impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile {
#[derive_message_formats]
fn message(&self) -> String {
format!("Too many newlines at the end of file")
}

fn fix_title(&self) -> String {
"Remove extraneous trailing newlines".to_string()
}
}

/// W391
pub(crate) fn too_many_newlines_at_end_of_file(
diagnostics: &mut Vec<Diagnostic>,
lxr: &[LexResult],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the Locator. I don't think we need to guard on empty files here -- that's just for the "no trailing newline" check, since empty files would be a false positive.

locator: &Locator,
) {
let source = locator.contents();

// Ignore empty and BOM only files
if source.is_empty() || source == "\u{feff}" {
return;
}

let mut count = 0;
let mut start_pos: Option<TextSize> = None;
let mut end_pos: Option<TextSize> = None;

for &(ref tok, range) in lxr.iter().rev().flatten() {
match tok {
Tok::NonLogicalNewline | Tok::Newline => {
if count == 0 {
end_pos = Some(range.end());
}
start_pos = Some(range.end());
count += 1;
}
Tok::Dedent => continue,
_ => {
break;
}
}
}

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);
}
}
@@ -0,0 +1,17 @@
---
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 |
| ^ W391
|
= help: Remove extraneous trailing newlines

ℹ 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 the end of file
|
12 | foo()
13 | bar()
14 | /
15 | |
16 | |
17 | |
|
= help: Remove extraneous 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 changes: 3 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions scripts/check_docs_formatted.py
Expand Up @@ -105,6 +105,7 @@
"tab-after-operator",
"tab-before-keyword",
"tab-before-operator",
"too-many-newlines-at-end-of-file",
"trailing-whitespace",
"unexpected-indentation",
]
Expand Down