diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W29.py b/crates/ruff/resources/test/fixtures/pycodestyle/W29.py new file mode 100644 index 0000000000..e9ad5800d8 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W29.py @@ -0,0 +1,26 @@ +#: Okay +# 情 +#: W291:1:6 +print +#: W293:2:1 +class Foo(object): + + bang = 12 +#: W291:2:35 +'''multiline +string with trailing whitespace''' +#: W291 W292 noeol +x = 1 +#: W191 W292 noeol +if False: + pass # indented with tabs +#: W292:1:36 noeol +# This line doesn't have a linefeed +#: W292:1:5 E225:1:2 noeol +1+ 1 +#: W292:1:27 E261:1:12 noeol +import this # no line feed +#: W292:3:22 noeol +class Test(object): + def __repr__(self): + return 'test' diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index cacc26632c..3a616bd049 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 = autofix.into() && settings.rules.should_fix(&Rule::UTF8EncodingDeclaration); @@ -139,6 +143,12 @@ 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, settings, autofix) { + 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 39a01475cb..d575a20af1 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 9544de5517..3ab1c1cced 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 @@ -786,7 +788,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 e9c60da535..583bf5aaa5 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("W29.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("W29.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 fac841d18f..a708c0d7c3 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 0000000000..46c5fb191a --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -0,0 +1,75 @@ +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, Rule}; +use crate::settings::{flags, Settings}; +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, + settings: &Settings, + autofix: flags::Autofix, +) -> Option { + let whitespace_count = line.chars().rev().take_while(|c| c.is_whitespace()).count(); + if whitespace_count > 0 { + let line_char_count = line.chars().count(); + let start = Location::new(lineno + 1, line_char_count - whitespace_count); + let end = Location::new(lineno + 1, line_char_count); + + if whitespace_count == line_char_count { + if settings.rules.enabled(&Rule::BlankLineContainsWhitespace) { + let mut diagnostic = + Diagnostic::new(BlankLineContainsWhitespace, Range::new(start, end)); + if matches!(autofix, flags::Autofix::Enabled) + && settings + .rules + .should_fix(&Rule::BlankLineContainsWhitespace) + { + diagnostic.amend(Fix::deletion(start, end)); + } + return Some(diagnostic); + } + } else if settings.rules.enabled(&Rule::TrailingWhitespace) { + let mut diagnostic = Diagnostic::new(TrailingWhitespace, Range::new(start, end)); + if matches!(autofix, flags::Autofix::Enabled) + && settings.rules.should_fix(&Rule::TrailingWhitespace) + { + diagnostic.amend(Fix::deletion(start, end)); + } + return Some(diagnostic); + } + } + None +} diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W29.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W29.py.snap new file mode 100644 index 0000000000..c88a113ee2 --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W29.py.snap @@ -0,0 +1,56 @@ +--- +source: crates/ruff/src/rules/pycodestyle/mod.rs +expression: diagnostics +--- +- kind: + TrailingWhitespace: ~ + location: + row: 4 + column: 5 + end_location: + row: 4 + column: 6 + fix: + content: "" + location: + row: 4 + column: 5 + end_location: + row: 4 + column: 6 + parent: ~ +- kind: + TrailingWhitespace: ~ + location: + row: 11 + column: 34 + end_location: + row: 11 + column: 37 + fix: + content: "" + location: + row: 11 + column: 34 + end_location: + row: 11 + column: 37 + parent: ~ +- kind: + TrailingWhitespace: ~ + location: + row: 13 + column: 5 + end_location: + row: 13 + column: 8 + fix: + content: "" + location: + row: 13 + column: 5 + end_location: + row: 13 + column: 8 + parent: ~ + diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W29.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W29.py.snap new file mode 100644 index 0000000000..f8f3c45533 --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W29.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pycodestyle/mod.rs +expression: diagnostics +--- +- kind: + BlankLineContainsWhitespace: ~ + location: + row: 7 + column: 0 + end_location: + row: 7 + column: 4 + fix: + content: "" + location: + row: 7 + column: 0 + end_location: + row: 7 + column: 4 + parent: ~ + diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 248af34ff1..153e7da7c2 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -465,7 +465,9 @@ mod tests { }]); let expected = FxHashSet::from_iter([ + Rule::TrailingWhitespace, Rule::NoNewLineAtEndOfFile, + Rule::BlankLineContainsWhitespace, Rule::DocLineTooLong, Rule::InvalidEscapeSequence, ]); @@ -483,7 +485,12 @@ mod tests { ignore: vec![codes::Pycodestyle::W292.into()], ..RuleSelection::default() }]); - let expected = FxHashSet::from_iter([Rule::DocLineTooLong, Rule::InvalidEscapeSequence]); + let expected = FxHashSet::from_iter([ + Rule::TrailingWhitespace, + Rule::BlankLineContainsWhitespace, + Rule::DocLineTooLong, + Rule::InvalidEscapeSequence, + ]); assert_eq!(actual, expected); let actual = resolve_rules([RuleSelection { @@ -514,7 +521,9 @@ mod tests { }, ]); let expected = FxHashSet::from_iter([ + Rule::TrailingWhitespace, Rule::NoNewLineAtEndOfFile, + Rule::BlankLineContainsWhitespace, Rule::DocLineTooLong, Rule::InvalidEscapeSequence, ]); @@ -549,7 +558,12 @@ mod tests { ..RuleSelection::default() }, ]); - let expected = FxHashSet::from_iter([Rule::DocLineTooLong, Rule::InvalidEscapeSequence]); + let expected = FxHashSet::from_iter([ + Rule::TrailingWhitespace, + Rule::BlankLineContainsWhitespace, + Rule::DocLineTooLong, + Rule::InvalidEscapeSequence, + ]); assert_eq!(actual, expected); let actual = resolve_rules([ @@ -564,7 +578,11 @@ mod tests { ..RuleSelection::default() }, ]); - let expected = FxHashSet::from_iter([Rule::InvalidEscapeSequence]); + let expected = FxHashSet::from_iter([ + Rule::TrailingWhitespace, + Rule::BlankLineContainsWhitespace, + Rule::InvalidEscapeSequence, + ]); assert_eq!(actual, expected); } } diff --git a/ruff.schema.json b/ruff.schema.json index 4da9a1ac59..d5d64083f9 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2100,7 +2100,9 @@ "W", "W2", "W29", + "W291", "W292", + "W293", "W5", "W50", "W505",