[pycodestyle] Implement redundant-backslash (E502) (#10292)

## Summary

Implements the
[redundant-backslash](https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes)
rule (E502) from pycodestyle.

## Test Plan

New fixture has been added

Part of #2402
This commit is contained in:
Auguste Lalande 2024-03-11 21:15:06 -04:00 committed by GitHub
parent fc7139d9a5
commit c746912b9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 475 additions and 4 deletions

View file

@ -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)

View file

@ -1,6 +1,7 @@
use crate::line_width::IndentWidth; use crate::line_width::IndentWidth;
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult; use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::TokenKind; use ruff_python_parser::TokenKind;
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -9,8 +10,8 @@ use ruff_text_size::{Ranged, TextRange};
use crate::registry::AsRule; use crate::registry::AsRule;
use crate::rules::pycodestyle::rules::logical_lines::{ use crate::rules::pycodestyle::rules::logical_lines::{
extraneous_whitespace, indentation, missing_whitespace, missing_whitespace_after_keyword, extraneous_whitespace, indentation, missing_whitespace, missing_whitespace_after_keyword,
missing_whitespace_around_operator, space_after_comma, space_around_operator, missing_whitespace_around_operator, redundant_backslash, space_after_comma,
whitespace_around_keywords, whitespace_around_named_parameter_equals, space_around_operator, whitespace_around_keywords, whitespace_around_named_parameter_equals,
whitespace_before_comment, whitespace_before_parameters, LogicalLines, TokenFlags, whitespace_before_comment, whitespace_before_parameters, LogicalLines, TokenFlags,
}; };
use crate::settings::LinterSettings; 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( pub(crate) fn check_logical_lines(
tokens: &[LexResult], tokens: &[LexResult],
locator: &Locator, locator: &Locator,
indexer: &Indexer,
stylist: &Stylist, stylist: &Stylist,
settings: &LinterSettings, settings: &LinterSettings,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
@ -73,6 +75,7 @@ pub(crate) fn check_logical_lines(
if line.flags().contains(TokenFlags::BRACKET) { if line.flags().contains(TokenFlags::BRACKET) {
whitespace_before_parameters(&line, &mut context); whitespace_before_parameters(&line, &mut context);
redundant_backslash(&line, locator, indexer, &mut context);
} }
// Extract the indentation level. // Extract the indentation level.

View file

@ -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, "E401") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleImportsOnOneLine),
(Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile), (Pycodestyle, "E402") => (RuleGroup::Stable, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
(Pycodestyle, "E501") => (RuleGroup::Stable, rules::pycodestyle::rules::LineTooLong), (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, "E701") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineColon),
(Pycodestyle, "E702") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon), (Pycodestyle, "E702") => (RuleGroup::Stable, rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon),
(Pycodestyle, "E703") => (RuleGroup::Stable, rules::pycodestyle::rules::UselessSemicolon), (Pycodestyle, "E703") => (RuleGroup::Stable, rules::pycodestyle::rules::UselessSemicolon),

View file

@ -132,7 +132,7 @@ pub fn check_path(
.any(|rule_code| rule_code.lint_source().is_logical_lines()) .any(|rule_code| rule_code.lint_source().is_logical_lines())
{ {
diagnostics.extend(crate::checkers::logical_lines::check_logical_lines( diagnostics.extend(crate::checkers::logical_lines::check_logical_lines(
&tokens, locator, stylist, settings, &tokens, locator, indexer, stylist, settings,
)); ));
} }

View file

@ -327,6 +327,7 @@ impl Rule {
| Rule::NoSpaceAfterBlockComment | Rule::NoSpaceAfterBlockComment
| Rule::NoSpaceAfterInlineComment | Rule::NoSpaceAfterInlineComment
| Rule::OverIndented | Rule::OverIndented
| Rule::RedundantBackslash
| Rule::TabAfterComma | Rule::TabAfterComma
| Rule::TabAfterKeyword | Rule::TabAfterKeyword
| Rule::TabAfterOperator | Rule::TabAfterOperator

View file

@ -71,6 +71,7 @@ mod tests {
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))] #[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::TypeComparison, Path::new("E721.py"))] #[test_case(Rule::TypeComparison, Path::new("E721.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.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<()> { fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!( let snapshot = format!(
"preview__{}_{}", "preview__{}_{}",

View file

@ -3,6 +3,7 @@ pub(crate) use indentation::*;
pub(crate) use missing_whitespace::*; pub(crate) use missing_whitespace::*;
pub(crate) use missing_whitespace_after_keyword::*; pub(crate) use missing_whitespace_after_keyword::*;
pub(crate) use missing_whitespace_around_operator::*; pub(crate) use missing_whitespace_around_operator::*;
pub(crate) use redundant_backslash::*;
pub(crate) use space_around_operator::*; pub(crate) use space_around_operator::*;
pub(crate) use whitespace_around_keywords::*; pub(crate) use whitespace_around_keywords::*;
pub(crate) use whitespace_around_named_parameter_equals::*; pub(crate) use whitespace_around_named_parameter_equals::*;
@ -25,6 +26,7 @@ mod indentation;
mod missing_whitespace; mod missing_whitespace;
mod missing_whitespace_after_keyword; mod missing_whitespace_after_keyword;
mod missing_whitespace_around_operator; mod missing_whitespace_around_operator;
mod redundant_backslash;
mod space_around_operator; mod space_around_operator;
mod whitespace_around_keywords; mod whitespace_around_keywords;
mod whitespace_around_named_parameter_equals; mod whitespace_around_named_parameter_equals;

View file

@ -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,
}
}
}

View file

@ -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)

View file

@ -380,7 +380,7 @@ per-file-ignores = { "__init__.py" = ["F401"] }
assert!(result.is_err()); assert!(result.is_err());
let result = PatternPrefixPair::from_str("**/bar:E501"); let result = PatternPrefixPair::from_str("**/bar:E501");
assert!(result.is_ok()); assert!(result.is_ok());
let result = PatternPrefixPair::from_str("bar:E502"); let result = PatternPrefixPair::from_str("bar:E503");
assert!(result.is_err()); assert!(result.is_err());
} }
} }

1
ruff.schema.json generated
View file

@ -2867,6 +2867,7 @@
"E5", "E5",
"E50", "E50",
"E501", "E501",
"E502",
"E7", "E7",
"E70", "E70",
"E701", "E701",

View file

@ -69,6 +69,7 @@ KNOWN_FORMATTING_VIOLATIONS = [
"over-indented", "over-indented",
"pass-statement-stub-body", "pass-statement-stub-body",
"prohibited-trailing-comma", "prohibited-trailing-comma",
"redundant-backslash",
"shebang-leading-whitespace", "shebang-leading-whitespace",
"surrounding-whitespace", "surrounding-whitespace",
"tab-indentation", "tab-indentation",