From 066f97bfedfda109ad48de1d67639b5c7c2bd483 Mon Sep 17 00:00:00 2001 From: mknaw Date: Tue, 21 Feb 2023 21:08:35 -0500 Subject: [PATCH] Implement pycodestyle whitespace lints W291, W293 --- .../test/fixtures/pycodestyle/W291.py | 10 +++ .../test/fixtures/pycodestyle/W293.py | 10 +++ crates/ruff/src/checkers/physical_lines.rs | 21 ++++++ crates/ruff/src/codes.rs | 2 + crates/ruff/src/registry.rs | 6 +- crates/ruff/src/rules/pycodestyle/mod.rs | 2 + .../ruff/src/rules/pycodestyle/rules/mod.rs | 4 ++ .../pycodestyle/rules/trailing_whitespace.rs | 69 +++++++++++++++++++ ...les__pycodestyle__tests__W291_W291.py.snap | 56 +++++++++++++++ ...les__pycodestyle__tests__W293_W293.py.snap | 22 ++++++ ruff.schema.json | 4 +- 11 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pycodestyle/W291.py create mode 100644 crates/ruff/resources/test/fixtures/pycodestyle/W293.py create mode 100644 crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs create mode 100644 crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W291.py.snap create mode 100644 crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W293.py.snap diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W291.py b/crates/ruff/resources/test/fixtures/pycodestyle/W291.py new file mode 100644 index 00000000000000..fae7e3cb0f7020 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W291.py @@ -0,0 +1,10 @@ +def first_func(): + # The line below has two spaces after its final character + pass + +# The line below has only spaces, but that's for W293 + +# Don't want trailing tabs either +x = 1000 +# Nor a mix of tabs and spaces +y = 100 diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W293.py b/crates/ruff/resources/test/fixtures/pycodestyle/W293.py new file mode 100644 index 00000000000000..1ec0121bc64f38 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W293.py @@ -0,0 +1,10 @@ +def first_func(): + # The line below has two spaces after its final character, but that's for W291 + pass + +# The line below has only spaces + +# Don't want trailing tabs either, but that's for W291 +x = 1000 +# Nor a mix of tabs and spaces, but that's for W291 +y = 100 diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index f141ad9e8d9f98..0bd9c5f69c54be 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -9,6 +9,7 @@ use crate::rules::flake8_executable::rules::{ }; use crate::rules::pycodestyle::rules::{ doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file, + trailing_whitespace, }; use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore}; use crate::rules::pylint; @@ -41,6 +42,9 @@ pub fn check_physical_lines( let enforce_unnecessary_coding_comment = settings.rules.enabled(&Rule::UTF8EncodingDeclaration); 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); + let enforce_blank_line_contains_whitespace = + settings.rules.enabled(&Rule::BlankLineContainsWhitespace); let fix_unnecessary_coding_comment = matches!(autofix, flags::Autofix::Enabled) && settings.rules.should_fix(&Rule::UTF8EncodingDeclaration); @@ -139,6 +143,23 @@ pub fn check_physical_lines( if enforce_bidirectional_unicode { diagnostics.extend(pylint::rules::bidirectional_unicode(index, line)); } + + if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace { + if let Some(diagnostic) = trailing_whitespace( + index, + line, + enforce_trailing_whitespace, + matches!(autofix, flags::Autofix::Enabled) + && settings.rules.should_fix(&Rule::TrailingWhitespace), + enforce_blank_line_contains_whitespace, + matches!(autofix, flags::Autofix::Enabled) + && settings + .rules + .should_fix(&Rule::BlankLineContainsWhitespace), + ) { + diagnostics.push(diagnostic); + } + } } if enforce_no_newline_at_end_of_file { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 2fe096ed22e424..a35071a3f79452 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -72,7 +72,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Pycodestyle, "E999") => Rule::SyntaxError, // pycodestyle warnings + (Pycodestyle, "W291") => Rule::TrailingWhitespace, (Pycodestyle, "W292") => Rule::NoNewLineAtEndOfFile, + (Pycodestyle, "W293") => Rule::BlankLineContainsWhitespace, (Pycodestyle, "W505") => Rule::DocLineTooLong, (Pycodestyle, "W605") => Rule::InvalidEscapeSequence, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 419994f23bc3c8..84a43acc5e0bc6 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -77,7 +77,9 @@ ruff_macros::register_rules!( rules::pycodestyle::rules::IOError, rules::pycodestyle::rules::SyntaxError, // pycodestyle warnings + rules::pycodestyle::rules::TrailingWhitespace, rules::pycodestyle::rules::NoNewLineAtEndOfFile, + rules::pycodestyle::rules::BlankLineContainsWhitespace, rules::pycodestyle::rules::DocLineTooLong, rules::pycodestyle::rules::InvalidEscapeSequence, // pyflakes @@ -782,7 +784,9 @@ impl Rule { | Rule::ShebangNewline | Rule::BidirectionalUnicode | Rule::ShebangPython - | Rule::ShebangWhitespace => &LintSource::PhysicalLines, + | Rule::ShebangWhitespace + | Rule::TrailingWhitespace + | Rule::BlankLineContainsWhitespace => &LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring | Rule::AmbiguousUnicodeCharacterString diff --git a/crates/ruff/src/rules/pycodestyle/mod.rs b/crates/ruff/src/rules/pycodestyle/mod.rs index e9c60da53599df..ffd25d9ae7d0a2 100644 --- a/crates/ruff/src/rules/pycodestyle/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/mod.rs @@ -24,6 +24,7 @@ mod tests { #[test_case(Rule::AmbiguousVariableName, Path::new("E741.py"))] #[test_case(Rule::LambdaAssignment, Path::new("E731.py"))] #[test_case(Rule::BareExcept, Path::new("E722.py"))] + #[test_case(Rule::BlankLineContainsWhitespace, Path::new("W293.py"))] #[test_case(Rule::InvalidEscapeSequence, Path::new("W605_0.py"))] #[test_case(Rule::InvalidEscapeSequence, Path::new("W605_1.py"))] #[test_case(Rule::LineTooLong, Path::new("E501.py"))] @@ -41,6 +42,7 @@ mod tests { #[test_case(Rule::NotInTest, Path::new("E713.py"))] #[test_case(Rule::NotIsTest, Path::new("E714.py"))] #[test_case(Rule::SyntaxError, Path::new("E999.py"))] + #[test_case(Rule::TrailingWhitespace, Path::new("W291.py"))] #[test_case(Rule::TrueFalseComparison, Path::new("E712.py"))] #[test_case(Rule::TypeComparison, Path::new("E721.py"))] #[test_case(Rule::UselessSemicolon, Path::new("E70.py"))] diff --git a/crates/ruff/src/rules/pycodestyle/rules/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/mod.rs index fac841d18f5830..a708c0d7c307a4 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/mod.rs @@ -32,6 +32,9 @@ pub use space_around_operator::{ space_around_operator, MultipleSpacesAfterOperator, MultipleSpacesBeforeOperator, TabAfterOperator, TabBeforeOperator, }; +pub use trailing_whitespace::{ + trailing_whitespace, BlankLineContainsWhitespace, TrailingWhitespace, +}; pub use type_comparison::{type_comparison, TypeComparison}; pub use whitespace_around_keywords::{ whitespace_around_keywords, MultipleSpacesAfterKeyword, MultipleSpacesBeforeKeyword, @@ -60,6 +63,7 @@ mod mixed_spaces_and_tabs; mod no_newline_at_end_of_file; mod not_tests; mod space_around_operator; +mod trailing_whitespace; mod type_comparison; mod whitespace_around_keywords; mod whitespace_before_comment; diff --git a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs new file mode 100644 index 00000000000000..1e1ce9e55319b4 --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -0,0 +1,69 @@ +use ruff_macros::{define_violation, derive_message_formats}; +use rustpython_parser::ast::Location; + +use crate::ast::types::Range; +use crate::fix::Fix; +use crate::registry::Diagnostic; +use crate::violation::AlwaysAutofixableViolation; + +define_violation!( + pub struct TrailingWhitespace; +); +impl AlwaysAutofixableViolation for TrailingWhitespace { + #[derive_message_formats] + fn message(&self) -> String { + format!("Trailing whitespace") + } + + fn autofix_title(&self) -> String { + "Remove trailing whitespace".to_string() + } +} + +define_violation!( + pub struct BlankLineContainsWhitespace; +); +impl AlwaysAutofixableViolation for BlankLineContainsWhitespace { + #[derive_message_formats] + fn message(&self) -> String { + format!("Blank line contains whitespace") + } + + fn autofix_title(&self) -> String { + "Remove whitespace from blank line".to_string() + } +} + +/// W291 & W293 +pub fn trailing_whitespace( + lineno: usize, + line: &str, + enforce_trailing_whitespace: bool, + fix_trailing_whitespace: bool, + enforce_blank_line_contains_whitespace: bool, + fix_blank_line_contains_whitespace: bool, +) -> Option { + let whitespace_count = line.chars().rev().take_while(|c| c.is_whitespace()).count(); + if whitespace_count > 0 { + let start = Location::new(lineno + 1, line.len() - whitespace_count); + let end = Location::new(lineno + 1, line.len()); + + if whitespace_count == line.len() { + if enforce_blank_line_contains_whitespace { + let mut diagnostic = + Diagnostic::new(BlankLineContainsWhitespace, Range::new(start, end)); + if fix_blank_line_contains_whitespace { + diagnostic.amend(Fix::deletion(start, end)); + } + return Some(diagnostic); + } + } else if enforce_trailing_whitespace { + let mut diagnostic = Diagnostic::new(TrailingWhitespace, Range::new(start, end)); + if fix_trailing_whitespace { + diagnostic.amend(Fix::deletion(start, end)); + } + return Some(diagnostic); + } + } + None +} diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W291.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W291.py.snap new file mode 100644 index 00000000000000..21a1b1dad56730 --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W291.py.snap @@ -0,0 +1,56 @@ +--- +source: crates/ruff/src/rules/pycodestyle/mod.rs +expression: diagnostics +--- +- kind: + TrailingWhitespace: ~ + location: + row: 3 + column: 8 + end_location: + row: 3 + column: 10 + fix: + content: "" + location: + row: 3 + column: 8 + end_location: + row: 3 + column: 10 + parent: ~ +- kind: + TrailingWhitespace: ~ + location: + row: 8 + column: 8 + end_location: + row: 8 + column: 9 + fix: + content: "" + location: + row: 8 + column: 8 + end_location: + row: 8 + column: 9 + parent: ~ +- kind: + TrailingWhitespace: ~ + location: + row: 10 + column: 7 + end_location: + row: 10 + column: 9 + fix: + content: "" + location: + row: 10 + column: 7 + end_location: + row: 10 + column: 9 + parent: ~ + diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W293.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W293.py.snap new file mode 100644 index 00000000000000..fb47af5ff63560 --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W293.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pycodestyle/mod.rs +expression: diagnostics +--- +- kind: + BlankLineContainsWhitespace: ~ + location: + row: 6 + column: 0 + end_location: + row: 6 + column: 4 + fix: + content: "" + location: + row: 6 + column: 0 + end_location: + row: 6 + column: 4 + parent: ~ + diff --git a/ruff.schema.json b/ruff.schema.json index 569f1c16f9b5bc..4953d8f58ab79d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2091,7 +2091,9 @@ "W", "W2", "W29", + "W291", "W292", + "W293", "W5", "W50", "W505", @@ -2151,4 +2153,4 @@ "type": "string" } } -} \ No newline at end of file +}