[pycodestyle] Whitespace after decorator (E204) (#12140)

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
This is the implementation for the new rule of `pycodestyle (E204)`. It
follows the guidlines described in the contributing site, and as such it
has a new file named `whitespace_after_decorator.rs`, a new test file
called `E204.py`, and as such invokes the `function` in the `AST
statement checker` for functions and functions in classes. Linking #2402
because it has all the pycodestyle rules.

## Test Plan

<!-- How was it tested? -->
The file E204.py, has a `decorator` defined called wrapper, and this
decorator is used for 2 cases. The first one is when a `function` which
has a `decorator` is called in the file, and the second one is when
there is a `class` and 2 `methods` are defined for the `class` with a
`decorator` attached it.

Test file:

``` python
def foo(fun):
    def wrapper():
        print('before')
        fun()
        print('after')
    return wrapper

# No error
@foo
def bar():
    print('bar')

# E204
@ foo
def baz():
    print('baz')

class Test:
    # No error
    @foo
    def bar(self):
        print('bar')

    # E204
    @ foo
    def baz(self):
        print('baz')
```

I am still new to rust and any suggestion is appreciated. Specially with
the way im using native ruff utilities.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Javier Kauer 2024-07-05 01:31:03 +02:00 committed by GitHub
parent 5e7ba05612
commit 1e07bfa373
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 181 additions and 0 deletions

View file

@ -0,0 +1,34 @@
def foo(fun):
def wrapper():
print('before')
fun()
print('after')
return wrapper
# No error
@foo
def bar():
print('bar')
# E204
@ foo
def baz():
print('baz')
class Test:
# No error
@foo
def bar(self):
print('bar')
# E204
@ foo
def baz(self):
print('baz')
# E204
@ \
foo
def baz():
print('baz')

View file

@ -368,6 +368,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnusedAsync) { if checker.enabled(Rule::UnusedAsync) {
ruff::rules::unused_async(checker, function_def); ruff::rules::unused_async(checker, function_def);
} }
if checker.enabled(Rule::WhitespaceAfterDecorator) {
pycodestyle::rules::whitespace_after_decorator(checker, decorator_list);
}
} }
Stmt::Return(_) => { Stmt::Return(_) => {
if checker.enabled(Rule::ReturnOutsideFunction) { if checker.enabled(Rule::ReturnOutsideFunction) {
@ -531,6 +534,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MetaClassABCMeta) { if checker.enabled(Rule::MetaClassABCMeta) {
refurb::rules::metaclass_abcmeta(checker, class_def); refurb::rules::metaclass_abcmeta(checker, class_def);
} }
if checker.enabled(Rule::WhitespaceAfterDecorator) {
pycodestyle::rules::whitespace_after_decorator(checker, decorator_list);
}
} }
Stmt::Import(ast::StmtImport { names, range: _ }) => { Stmt::Import(ast::StmtImport { names, range: _ }) => {
if checker.enabled(Rule::MultipleImportsOnOneLine) { if checker.enabled(Rule::MultipleImportsOnOneLine) {

View file

@ -78,6 +78,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pycodestyle, "E201") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket), (Pycodestyle, "E201") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
(Pycodestyle, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket), (Pycodestyle, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
(Pycodestyle, "E203") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation), (Pycodestyle, "E203") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
(Pycodestyle, "E204") => (RuleGroup::Preview, rules::pycodestyle::rules::WhitespaceAfterDecorator),
(Pycodestyle, "E211") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters), (Pycodestyle, "E211") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
(Pycodestyle, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator), (Pycodestyle, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
(Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator), (Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),

View file

@ -58,6 +58,7 @@ mod tests {
#[test_case(Rule::TypeComparison, Path::new("E721.py"))] #[test_case(Rule::TypeComparison, Path::new("E721.py"))]
#[test_case(Rule::UselessSemicolon, Path::new("E70.py"))] #[test_case(Rule::UselessSemicolon, Path::new("E70.py"))]
#[test_case(Rule::UselessSemicolon, Path::new("E703.ipynb"))] #[test_case(Rule::UselessSemicolon, Path::new("E703.ipynb"))]
#[test_case(Rule::WhitespaceAfterDecorator, Path::new("E204.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path( let diagnostics = test_path(

View file

@ -20,6 +20,7 @@ pub(crate) use tab_indentation::*;
pub(crate) use too_many_newlines_at_end_of_file::*; pub(crate) use too_many_newlines_at_end_of_file::*;
pub(crate) use trailing_whitespace::*; pub(crate) use trailing_whitespace::*;
pub(crate) use type_comparison::*; pub(crate) use type_comparison::*;
pub(crate) use whitespace_after_decorator::*;
mod ambiguous_class_name; mod ambiguous_class_name;
mod ambiguous_function_name; mod ambiguous_function_name;
@ -43,3 +44,4 @@ mod tab_indentation;
mod too_many_newlines_at_end_of_file; mod too_many_newlines_at_end_of_file;
mod trailing_whitespace; mod trailing_whitespace;
mod type_comparison; mod type_comparison;
mod whitespace_after_decorator;

View file

@ -0,0 +1,71 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Decorator;
use ruff_python_trivia::is_python_whitespace;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for trailing whitespace after a decorator's opening `@`.
///
/// ## Why is this bad?
/// Including whitespace after the `@` symbol is not compliant with
/// [PEP 8].
///
/// ## Example
///
/// ```python
/// @ decorator
/// def func():
/// pass
/// ```
///
/// Use instead:
/// ```python
/// @decorator
/// def func():
/// pass
/// ```
///
/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length
#[violation]
pub struct WhitespaceAfterDecorator;
impl AlwaysFixableViolation for WhitespaceAfterDecorator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Whitespace after decorator")
}
fn fix_title(&self) -> String {
"Remove whitespace".to_string()
}
}
/// E204
pub(crate) fn whitespace_after_decorator(checker: &mut Checker, decorator_list: &[Decorator]) {
for decorator in decorator_list {
let decorator_text = checker.locator().slice(decorator);
// Determine whether the `@` is followed by whitespace.
if let Some(trailing) = decorator_text.strip_prefix('@') {
// Collect the whitespace characters after the `@`.
if trailing.chars().next().is_some_and(is_python_whitespace) {
let end = trailing
.chars()
.position(|c| !(is_python_whitespace(c) || matches!(c, '\n' | '\r' | '\\')))
.unwrap_or(trailing.len());
let start = decorator.start() + TextSize::from(1);
let end = start + TextSize::try_from(end).unwrap();
let range = TextRange::new(start, end);
let mut diagnostic = Diagnostic::new(WhitespaceAfterDecorator, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range)));
checker.diagnostics.push(diagnostic);
}
}
}
}

View file

@ -0,0 +1,64 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E204.py:14:2: E204 [*] Whitespace after decorator
|
13 | # E204
14 | @ foo
| ^ E204
15 | def baz():
16 | print('baz')
|
= help: Remove whitespace
Safe fix
11 11 | print('bar')
12 12 |
13 13 | # E204
14 |-@ foo
14 |+@foo
15 15 | def baz():
16 16 | print('baz')
17 17 |
E204.py:25:6: E204 [*] Whitespace after decorator
|
24 | # E204
25 | @ foo
| ^ E204
26 | def baz(self):
27 | print('baz')
|
= help: Remove whitespace
Safe fix
22 22 | print('bar')
23 23 |
24 24 | # E204
25 |- @ foo
25 |+ @foo
26 26 | def baz(self):
27 27 | print('baz')
28 28 |
E204.py:31:2: E204 [*] Whitespace after decorator
|
30 | # E204
31 | @ \
| __^
32 | | foo
| |_^ E204
33 | def baz():
34 | print('baz')
|
= help: Remove whitespace
Safe fix
28 28 |
29 29 |
30 30 | # E204
31 |-@ \
32 |-foo
31 |+@foo
33 32 | def baz():
34 33 | print('baz')

1
ruff.schema.json generated
View file

@ -2904,6 +2904,7 @@
"E201", "E201",
"E202", "E202",
"E203", "E203",
"E204",
"E21", "E21",
"E211", "E211",
"E22", "E22",

View file

@ -86,6 +86,7 @@ KNOWN_FORMATTING_VIOLATIONS = [
"unnecessary-class-parentheses", "unnecessary-class-parentheses",
"unnecessary-escaped-quote", "unnecessary-escaped-quote",
"useless-semicolon", "useless-semicolon",
"whitespace-after-decorator",
"whitespace-after-open-bracket", "whitespace-after-open-bracket",
"whitespace-before-close-bracket", "whitespace-before-close-bracket",
"whitespace-before-parameters", "whitespace-before-parameters",