diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E502.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E502.py new file mode 100644 index 0000000000000..aa7348768566e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E502.py @@ -0,0 +1,88 @@ +a = 2 + 2 + +a = (2 + 2) + +a = 2 + \ + 3 \ + + 4 + +a = (3 -\ + 2 + \ + 7) + +z = 5 + \ + (3 -\ + 2 + \ + 7) + \ + 4 + +b = [2 + + 2] + +b = [ + 2 + 4 + 5 + \ + 44 \ + - 5 +] + +c = (True and + False \ + or False \ + and True \ +) + +c = (True and + False) + +d = True and \ + False or \ + False \ + and not True + + +s = { + 'x': 2 + \ + 2 +} + + +s = { + 'x': 2 + + 2 +} + + +x = {2 + 4 \ + + 3} + +y = ( + 2 + 2 # \ + + 3 # \ + + 4 \ + + 3 +) + + +x = """ + (\\ + ) +""" + + +("""hello \ +""") + +("hello \ +") + + +x = "abc" \ + "xyz" + +x = ("abc" \ + "xyz") + + +def foo(): + x = (a + \ + 2) diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index dc72a4834e99f..4044e6c18a67b 100644 --- a/crates/ruff_linter/src/checkers/logical_lines.rs +++ b/crates/ruff_linter/src/checkers/logical_lines.rs @@ -1,6 +1,7 @@ use crate::line_width::IndentWidth; use ruff_diagnostics::Diagnostic; use ruff_python_codegen::Stylist; +use ruff_python_index::Indexer; use ruff_python_parser::lexer::LexResult; use ruff_python_parser::TokenKind; use ruff_source_file::Locator; @@ -9,8 +10,8 @@ use ruff_text_size::{Ranged, TextRange}; use crate::registry::AsRule; use crate::rules::pycodestyle::rules::logical_lines::{ extraneous_whitespace, indentation, missing_whitespace, missing_whitespace_after_keyword, - missing_whitespace_around_operator, space_after_comma, space_around_operator, - whitespace_around_keywords, whitespace_around_named_parameter_equals, + missing_whitespace_around_operator, redundant_backslash, space_after_comma, + space_around_operator, whitespace_around_keywords, whitespace_around_named_parameter_equals, whitespace_before_comment, whitespace_before_parameters, LogicalLines, TokenFlags, }; use crate::settings::LinterSettings; @@ -35,6 +36,7 @@ pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize { pub(crate) fn check_logical_lines( tokens: &[LexResult], locator: &Locator, + indexer: &Indexer, stylist: &Stylist, settings: &LinterSettings, ) -> Vec { @@ -73,6 +75,7 @@ pub(crate) fn check_logical_lines( if line.flags().contains(TokenFlags::BRACKET) { whitespace_before_parameters(&line, &mut context); + redundant_backslash(&line, locator, indexer, &mut context); } // Extract the indentation level. diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 4f545d1afae88..fb38225dfb63b 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -146,6 +146,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pycodestyle, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine), (Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile), (Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong), + (Pycodestyle, "E502") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::RedundantBackslash), (Pycodestyle, "E701") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineColon), (Pycodestyle, "E702") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon), (Pycodestyle, "E703") => (RuleGroup::Stable, rules::pycodestyle::rules::UselessSemicolon), diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 66f98ef3bf7ef..4f5de73ae9686 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -132,7 +132,7 @@ pub fn check_path( .any(|rule_code| rule_code.lint_source().is_logical_lines()) { diagnostics.extend(crate::checkers::logical_lines::check_logical_lines( - &tokens, locator, stylist, settings, + &tokens, locator, indexer, stylist, settings, )); } diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index e85f14d7116af..1ae0df64de0eb 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -327,6 +327,7 @@ impl Rule { | Rule::NoSpaceAfterBlockComment | Rule::NoSpaceAfterInlineComment | Rule::OverIndented + | Rule::RedundantBackslash | Rule::TabAfterComma | Rule::TabAfterKeyword | Rule::TabAfterOperator diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index a6d850f862aef..f9ed1de0f8884 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -71,6 +71,7 @@ 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::RedundantBackslash, Path::new("E502.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs index 329116eca9b1d..606972bcf0c38 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -3,6 +3,7 @@ pub(crate) use indentation::*; pub(crate) use missing_whitespace::*; pub(crate) use missing_whitespace_after_keyword::*; pub(crate) use missing_whitespace_around_operator::*; +pub(crate) use redundant_backslash::*; pub(crate) use space_around_operator::*; pub(crate) use whitespace_around_keywords::*; pub(crate) use whitespace_around_named_parameter_equals::*; @@ -25,6 +26,7 @@ mod indentation; mod missing_whitespace; mod missing_whitespace_after_keyword; mod missing_whitespace_around_operator; +mod redundant_backslash; mod space_around_operator; mod whitespace_around_keywords; mod whitespace_around_named_parameter_equals; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs new file mode 100644 index 0000000000000..b493c47605a46 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs @@ -0,0 +1,92 @@ +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_index::Indexer; +use ruff_python_parser::TokenKind; +use ruff_source_file::Locator; +use ruff_text_size::{Ranged, TextRange, TextSize}; + +use crate::checkers::logical_lines::LogicalLinesContext; + +use super::LogicalLine; + +/// ## What it does +/// Checks for redundant backslashes between brackets. +/// +/// ## Why is this bad? +/// Explicit line joins using a backslash are redundant between brackets. +/// +/// ## Example +/// ```python +/// x = (2 + \ +/// 2) +/// ``` +/// +/// Use instead: +/// ```python +/// x = (2 + +/// 2) +/// ``` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length +#[violation] +pub struct RedundantBackslash; + +impl AlwaysFixableViolation for RedundantBackslash { + #[derive_message_formats] + fn message(&self) -> String { + format!("Redundant backslash") + } + + fn fix_title(&self) -> String { + "Remove redundant backslash".to_string() + } +} + +/// E502 +pub(crate) fn redundant_backslash( + line: &LogicalLine, + locator: &Locator, + indexer: &Indexer, + context: &mut LogicalLinesContext, +) { + let mut parens = 0; + let continuation_lines = indexer.continuation_line_starts(); + let mut start_index = 0; + + for token in line.tokens() { + match token.kind() { + TokenKind::Lpar | TokenKind::Lsqb | TokenKind::Lbrace => { + if parens == 0 { + let start = locator.line_start(token.start()); + start_index = continuation_lines + .binary_search(&start) + .map_or_else(|err_index| err_index, |ok_index| ok_index); + } + parens += 1; + } + TokenKind::Rpar | TokenKind::Rsqb | TokenKind::Rbrace => { + parens -= 1; + if parens == 0 { + let end = locator.line_start(token.start()); + let end_index = continuation_lines + .binary_search(&end) + .map_or_else(|err_index| err_index, |ok_index| ok_index); + for continuation_line in &continuation_lines[start_index..end_index] { + let backslash_end = locator.line_end(*continuation_line); + let backslash_start = backslash_end - TextSize::new(1); + let mut diagnostic = Diagnostic::new( + RedundantBackslash, + TextRange::new(backslash_start, backslash_end), + ); + diagnostic.set_fix(Fix::safe_edit(Edit::deletion( + backslash_start, + backslash_end, + ))); + context.push_diagnostic(diagnostic); + } + } + } + _ => continue, + } + } +} diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E502_E502.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E502_E502.py.snap new file mode 100644 index 0000000000000..6862f6161fdb1 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E502_E502.py.snap @@ -0,0 +1,281 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +E502.py:9:9: E502 [*] Redundant backslash + | + 7 | + 4 + 8 | + 9 | a = (3 -\ + | ^ E502 +10 | 2 + \ +11 | 7) + | + = help: Remove redundant backslash + +ℹ Safe fix +6 6 | 3 \ +7 7 | + 4 +8 8 | +9 |-a = (3 -\ + 9 |+a = (3 - +10 10 | 2 + \ +11 11 | 7) +12 12 | + +E502.py:10:11: E502 [*] Redundant backslash + | + 9 | a = (3 -\ +10 | 2 + \ + | ^ E502 +11 | 7) + | + = help: Remove redundant backslash + +ℹ Safe fix +7 7 | + 4 +8 8 | +9 9 | a = (3 -\ +10 |- 2 + \ + 10 |+ 2 + +11 11 | 7) +12 12 | +13 13 | z = 5 + \ + +E502.py:14:9: E502 [*] Redundant backslash + | +13 | z = 5 + \ +14 | (3 -\ + | ^ E502 +15 | 2 + \ +16 | 7) + \ + | + = help: Remove redundant backslash + +ℹ Safe fix +11 11 | 7) +12 12 | +13 13 | z = 5 + \ +14 |- (3 -\ + 14 |+ (3 - +15 15 | 2 + \ +16 16 | 7) + \ +17 17 | 4 + +E502.py:15:11: E502 [*] Redundant backslash + | +13 | z = 5 + \ +14 | (3 -\ +15 | 2 + \ + | ^ E502 +16 | 7) + \ +17 | 4 + | + = help: Remove redundant backslash + +ℹ Safe fix +12 12 | +13 13 | z = 5 + \ +14 14 | (3 -\ +15 |- 2 + \ + 15 |+ 2 + +16 16 | 7) + \ +17 17 | 4 +18 18 | + +E502.py:23:17: E502 [*] Redundant backslash + | +22 | b = [ +23 | 2 + 4 + 5 + \ + | ^ E502 +24 | 44 \ +25 | - 5 + | + = help: Remove redundant backslash + +ℹ Safe fix +20 20 | 2] +21 21 | +22 22 | b = [ +23 |- 2 + 4 + 5 + \ + 23 |+ 2 + 4 + 5 + +24 24 | 44 \ +25 25 | - 5 +26 26 | ] + +E502.py:24:8: E502 [*] Redundant backslash + | +22 | b = [ +23 | 2 + 4 + 5 + \ +24 | 44 \ + | ^ E502 +25 | - 5 +26 | ] + | + = help: Remove redundant backslash + +ℹ Safe fix +21 21 | +22 22 | b = [ +23 23 | 2 + 4 + 5 + \ +24 |- 44 \ + 24 |+ 44 +25 25 | - 5 +26 26 | ] +27 27 | + +E502.py:29:11: E502 [*] Redundant backslash + | +28 | c = (True and +29 | False \ + | ^ E502 +30 | or False \ +31 | and True \ + | + = help: Remove redundant backslash + +ℹ Safe fix +26 26 | ] +27 27 | +28 28 | c = (True and +29 |- False \ + 29 |+ False +30 30 | or False \ +31 31 | and True \ +32 32 | ) + +E502.py:30:14: E502 [*] Redundant backslash + | +28 | c = (True and +29 | False \ +30 | or False \ + | ^ E502 +31 | and True \ +32 | ) + | + = help: Remove redundant backslash + +ℹ Safe fix +27 27 | +28 28 | c = (True and +29 29 | False \ +30 |- or False \ + 30 |+ or False +31 31 | and True \ +32 32 | ) +33 33 | + +E502.py:31:14: E502 [*] Redundant backslash + | +29 | False \ +30 | or False \ +31 | and True \ + | ^ E502 +32 | ) + | + = help: Remove redundant backslash + +ℹ Safe fix +28 28 | c = (True and +29 29 | False \ +30 30 | or False \ +31 |- and True \ + 31 |+ and True +32 32 | ) +33 33 | +34 34 | c = (True and + +E502.py:44:14: E502 [*] Redundant backslash + | +43 | s = { +44 | 'x': 2 + \ + | ^ E502 +45 | 2 +46 | } + | + = help: Remove redundant backslash + +ℹ Safe fix +41 41 | +42 42 | +43 43 | s = { +44 |- 'x': 2 + \ + 44 |+ 'x': 2 + +45 45 | 2 +46 46 | } +47 47 | + +E502.py:55:12: E502 [*] Redundant backslash + | +55 | x = {2 + 4 \ + | ^ E502 +56 | + 3} + | + = help: Remove redundant backslash + +ℹ Safe fix +52 52 | } +53 53 | +54 54 | +55 |-x = {2 + 4 \ + 55 |+x = {2 + 4 +56 56 | + 3} +57 57 | +58 58 | y = ( + +E502.py:61:9: E502 [*] Redundant backslash + | +59 | 2 + 2 # \ +60 | + 3 # \ +61 | + 4 \ + | ^ E502 +62 | + 3 +63 | ) + | + = help: Remove redundant backslash + +ℹ Safe fix +58 58 | y = ( +59 59 | 2 + 2 # \ +60 60 | + 3 # \ +61 |- + 4 \ + 61 |+ + 4 +62 62 | + 3 +63 63 | ) +64 64 | + +E502.py:82:12: E502 [*] Redundant backslash + | +80 | "xyz" +81 | +82 | x = ("abc" \ + | ^ E502 +83 | "xyz") + | + = help: Remove redundant backslash + +ℹ Safe fix +79 79 | x = "abc" \ +80 80 | "xyz" +81 81 | +82 |-x = ("abc" \ + 82 |+x = ("abc" +83 83 | "xyz") +84 84 | +85 85 | + +E502.py:87:14: E502 [*] Redundant backslash + | +86 | def foo(): +87 | x = (a + \ + | ^ E502 +88 | 2) + | + = help: Remove redundant backslash + +ℹ Safe fix +84 84 | +85 85 | +86 86 | def foo(): +87 |- x = (a + \ + 87 |+ x = (a + +88 88 | 2) diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index c5d610f0c7fd7..75a6d13243f51 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -380,7 +380,7 @@ per-file-ignores = { "__init__.py" = ["F401"] } assert!(result.is_err()); let result = PatternPrefixPair::from_str("**/bar:E501"); assert!(result.is_ok()); - let result = PatternPrefixPair::from_str("bar:E502"); + let result = PatternPrefixPair::from_str("bar:E503"); assert!(result.is_err()); } } diff --git a/ruff.schema.json b/ruff.schema.json index 7509cb026b855..9118cd6c45ce8 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2867,6 +2867,7 @@ "E5", "E50", "E501", + "E502", "E7", "E70", "E701", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 234fb825e67a9..cfb039093237c 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -69,6 +69,7 @@ "over-indented", "pass-statement-stub-body", "prohibited-trailing-comma", + "redundant-backslash", "shebang-leading-whitespace", "surrounding-whitespace", "tab-indentation",