diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/E24.py b/crates/ruff/resources/test/fixtures/pycodestyle/E24.py new file mode 100644 index 0000000000..36fb4aa7eb --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pycodestyle/E24.py @@ -0,0 +1,13 @@ +#: E241 +a = (1, 2) +#: Okay +b = (1, 20) +#: E242 +a = (1, 2) # tab before 2 +#: Okay +b = (1, 20) # space before 20 +#: E241 E241 E241 +# issue 135 +more_spaces = [a, b, + ef, +h, + c, -d] diff --git a/crates/ruff/src/checkers/logical_lines.rs b/crates/ruff/src/checkers/logical_lines.rs index a27b52c3ce..95cad2f647 100644 --- a/crates/ruff/src/checkers/logical_lines.rs +++ b/crates/ruff/src/checkers/logical_lines.rs @@ -9,9 +9,9 @@ use ruff_source_file::Locator; use crate::registry::{AsRule, Rule}; use crate::rules::pycodestyle::rules::logical_lines::{ extraneous_whitespace, indentation, missing_whitespace, missing_whitespace_after_keyword, - missing_whitespace_around_operator, space_around_operator, whitespace_around_keywords, - whitespace_around_named_parameter_equals, whitespace_before_comment, - whitespace_before_parameters, LogicalLines, TokenFlags, + missing_whitespace_around_operator, 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::Settings; @@ -61,6 +61,9 @@ pub(crate) fn check_logical_lines( missing_whitespace_around_operator(&line, &mut context); missing_whitespace(&line, should_fix_missing_whitespace, &mut context); } + if line.flags().contains(TokenFlags::PUNCTUATION) { + space_after_comma(&line, &mut context); + } if line .flags() diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 1be27936c1..c00f576a38 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -84,6 +84,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pycodestyle, "E227") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator), (Pycodestyle, "E228") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator), (Pycodestyle, "E231") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespace), + (Pycodestyle, "E241") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma), + (Pycodestyle, "E242") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterComma), (Pycodestyle, "E251") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals), (Pycodestyle, "E252") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals), (Pycodestyle, "E261") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment), diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 6112c6e4c0..dcede096b1 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -303,6 +303,7 @@ impl Rule { | Rule::MissingWhitespaceAroundOperator | Rule::MissingWhitespaceAroundParameterEquals | Rule::MultipleLeadingHashesForBlockComment + | Rule::MultipleSpacesAfterComma | Rule::MultipleSpacesAfterKeyword | Rule::MultipleSpacesAfterOperator | Rule::MultipleSpacesBeforeKeyword @@ -312,6 +313,7 @@ impl Rule { | Rule::NoSpaceAfterBlockComment | Rule::NoSpaceAfterInlineComment | Rule::OverIndented + | Rule::TabAfterComma | Rule::TabAfterKeyword | Rule::TabAfterOperator | Rule::TabBeforeKeyword diff --git a/crates/ruff/src/rules/pycodestyle/mod.rs b/crates/ruff/src/rules/pycodestyle/mod.rs index cc25708246..9810feb01c 100644 --- a/crates/ruff/src/rules/pycodestyle/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/mod.rs @@ -70,6 +70,7 @@ mod tests { #[test_case(Rule::IndentationWithInvalidMultiple, Path::new("E11.py"))] #[test_case(Rule::IndentationWithInvalidMultipleComment, Path::new("E11.py"))] #[test_case(Rule::MultipleLeadingHashesForBlockComment, Path::new("E26.py"))] + #[test_case(Rule::MultipleSpacesAfterComma, Path::new("E24.py"))] #[test_case(Rule::MultipleSpacesAfterKeyword, Path::new("E27.py"))] #[test_case(Rule::MultipleSpacesAfterOperator, Path::new("E22.py"))] #[test_case(Rule::MultipleSpacesBeforeKeyword, Path::new("E27.py"))] @@ -80,6 +81,7 @@ mod tests { #[test_case(Rule::NoSpaceAfterBlockComment, Path::new("E26.py"))] #[test_case(Rule::NoSpaceAfterInlineComment, Path::new("E26.py"))] #[test_case(Rule::OverIndented, Path::new("E11.py"))] + #[test_case(Rule::TabAfterComma, Path::new("E24.py"))] #[test_case(Rule::TabAfterKeyword, Path::new("E27.py"))] #[test_case(Rule::TabAfterOperator, Path::new("E22.py"))] #[test_case(Rule::TabBeforeKeyword, Path::new("E27.py"))] diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs index a20be973e6..c6c4b7dcde 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs @@ -120,6 +120,57 @@ impl Violation for MultipleSpacesAfterOperator { } } +/// ## What it does +/// Checks for extraneous tabs after a comma. +/// +/// ## Why is this bad? +/// Commas should be followed by one space, never tabs. +/// +/// ## Example +/// ```python +/// a = 4,\t5 +/// ``` +/// +/// Use instead: +/// ```python +/// a = 4, 3 +/// ``` +/// +#[violation] +pub struct TabAfterComma; + +impl Violation for TabAfterComma { + #[derive_message_formats] + fn message(&self) -> String { + format!("Tab after comma") + } +} + +/// ## What it does +/// Checks for extraneous whitespace after a comma. +/// +/// ## Why is this bad? +/// According to the `black` code style, commas should be followed by a single space. +/// +/// ## Example +/// ```python +/// a = 4, 5 +/// ``` +/// +/// Use instead: +/// ```python +/// a = 4, 5 +/// ``` +#[violation] +pub struct MultipleSpacesAfterComma; + +impl Violation for MultipleSpacesAfterComma { + #[derive_message_formats] + fn message(&self) -> String { + format!("Multiple spaces after comma") + } +} + /// E221, E222, E223, E224 pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLinesContext) { let mut after_operator = false; @@ -161,6 +212,23 @@ pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLin } } +/// E241, E242 +pub(crate) fn space_after_comma(line: &LogicalLine, context: &mut LogicalLinesContext) { + for token in line.tokens() { + if matches!(token.kind(), TokenKind::Comma) { + match line.trailing_whitespace(token) { + (Whitespace::Tab, len) => { + context.push(TabAfterComma, TextRange::at(token.end(), len)); + } + (Whitespace::Many, len) => { + context.push(MultipleSpacesAfterComma, TextRange::at(token.end(), len)); + } + _ => {} + } + } + } +} + const fn is_operator_token(token: TokenKind) -> bool { matches!( token, diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E241_E24.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E241_E24.py.snap new file mode 100644 index 0000000000..db80143017 --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E241_E24.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff/src/rules/pycodestyle/mod.rs +--- +E24.py:2:8: E241 Multiple spaces after comma + | +1 | #: E241 +2 | a = (1, 2) + | ^^ E241 +3 | #: Okay +4 | b = (1, 20) + | + +E24.py:11:18: E241 Multiple spaces after comma + | + 9 | #: E241 E241 E241 +10 | # issue 135 +11 | more_spaces = [a, b, + | ^^^^ E241 +12 | ef, +h, +13 | c, -d] + | + +E24.py:12:19: E241 Multiple spaces after comma + | +10 | # issue 135 +11 | more_spaces = [a, b, +12 | ef, +h, + | ^^ E241 +13 | c, -d] + | + +E24.py:13:18: E241 Multiple spaces after comma + | +11 | more_spaces = [a, b, +12 | ef, +h, +13 | c, -d] + | ^^^ E241 + | + + diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E242_E24.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E242_E24.py.snap new file mode 100644 index 0000000000..63298792db --- /dev/null +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E242_E24.py.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/rules/pycodestyle/mod.rs +--- +E24.py:6:8: E242 Tab after comma + | +4 | b = (1, 20) +5 | #: E242 +6 | a = (1, 2) # tab before 2 + | ^ E242 +7 | #: Okay +8 | b = (1, 20) # space before 20 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index d6dad0c57b..f56257eb82 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1869,6 +1869,8 @@ "E227", "E228", "E231", + "E241", + "E242", "E251", "E252", "E261", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 686ff1f5bf..c18d7a86c9 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -43,6 +43,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "missing-whitespace-around-operator", "multi-line-implicit-string-concatenation", "multiple-leading-hashes-for-block-comment", + "multiple-spaces-after-comma", "multiple-spaces-after-keyword", "multiple-spaces-after-operator", "multiple-spaces-before-keyword", @@ -81,6 +82,7 @@ KNOWN_PARSE_ERRORS = [ "missing-newline-at-end-of-file", "mixed-spaces-and-tabs", "no-indented-block", + "tab-after-comma", "tab-after-keyword", "tab-after-operator", "tab-before-keyword",