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
16 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_0.py
@@ -0,0 +1,16 @@
# 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()
10 changes: 9 additions & 1 deletion crates/ruff_linter/src/checkers/physical_lines.rs
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Expand Up @@ -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),

Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/registry.rs
Expand Up @@ -254,6 +254,7 @@ impl Rule {
| Rule::MissingCopyrightNotice
| Rule::MissingNewlineAtEndOfFile
| Rule::MixedSpacesAndTabs
| Rule::TooManyNewlinesAtEndOfFile
| Rule::TrailingWhitespace => LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AvoidableEscapedQuote
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/rules/pycodestyle/mod.rs
Expand Up @@ -71,6 +71,10 @@ 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"))]
#[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__{}_{}",
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,63 @@
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_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;

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(locator: &Locator) -> Option<Diagnostic> {
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"(\r\n){2,}$|\n{2,}$|\r{2,}$").unwrap();

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());

// Calculate the TextRange to keep only one newline at the end
let range = TextRange::new(start_pos, end_pos);
let mut diagnostic = Diagnostic::new(TooManyNewlinesAtEndOfFile, range);
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(start_pos, end_pos)));
return Some(diagnostic);
}

None
}
MichaReiser marked this conversation as resolved.
Show resolved Hide resolved
@@ -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 |-
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

@@ -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()
@@ -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 @@ -104,6 +104,7 @@
"tab-after-operator",
"tab-before-keyword",
"tab-before-operator",
"too-many-newlines-at-end-of-file",
"trailing-whitespace",
"unexpected-indentation",
]
Expand Down