Skip to content

Commit 1e07bfa

Browse files
[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 <[email protected]>
1 parent 5e7ba05 commit 1e07bfa

File tree

9 files changed

+181
-0
lines changed

9 files changed

+181
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
def foo(fun):
2+
def wrapper():
3+
print('before')
4+
fun()
5+
print('after')
6+
return wrapper
7+
8+
# No error
9+
@foo
10+
def bar():
11+
print('bar')
12+
13+
# E204
14+
@ foo
15+
def baz():
16+
print('baz')
17+
18+
class Test:
19+
# No error
20+
@foo
21+
def bar(self):
22+
print('bar')
23+
24+
# E204
25+
@ foo
26+
def baz(self):
27+
print('baz')
28+
29+
30+
# E204
31+
@ \
32+
foo
33+
def baz():
34+
print('baz')

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

+6
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
368368
if checker.enabled(Rule::UnusedAsync) {
369369
ruff::rules::unused_async(checker, function_def);
370370
}
371+
if checker.enabled(Rule::WhitespaceAfterDecorator) {
372+
pycodestyle::rules::whitespace_after_decorator(checker, decorator_list);
373+
}
371374
}
372375
Stmt::Return(_) => {
373376
if checker.enabled(Rule::ReturnOutsideFunction) {
@@ -531,6 +534,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
531534
if checker.enabled(Rule::MetaClassABCMeta) {
532535
refurb::rules::metaclass_abcmeta(checker, class_def);
533536
}
537+
if checker.enabled(Rule::WhitespaceAfterDecorator) {
538+
pycodestyle::rules::whitespace_after_decorator(checker, decorator_list);
539+
}
534540
}
535541
Stmt::Import(ast::StmtImport { names, range: _ }) => {
536542
if checker.enabled(Rule::MultipleImportsOnOneLine) {

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
7878
(Pycodestyle, "E201") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
7979
(Pycodestyle, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
8080
(Pycodestyle, "E203") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
81+
(Pycodestyle, "E204") => (RuleGroup::Preview, rules::pycodestyle::rules::WhitespaceAfterDecorator),
8182
(Pycodestyle, "E211") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
8283
(Pycodestyle, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
8384
(Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),

crates/ruff_linter/src/rules/pycodestyle/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ mod tests {
5858
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
5959
#[test_case(Rule::UselessSemicolon, Path::new("E70.py"))]
6060
#[test_case(Rule::UselessSemicolon, Path::new("E703.ipynb"))]
61+
#[test_case(Rule::WhitespaceAfterDecorator, Path::new("E204.py"))]
6162
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
6263
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
6364
let diagnostics = test_path(

crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub(crate) use tab_indentation::*;
2020
pub(crate) use too_many_newlines_at_end_of_file::*;
2121
pub(crate) use trailing_whitespace::*;
2222
pub(crate) use type_comparison::*;
23+
pub(crate) use whitespace_after_decorator::*;
2324

2425
mod ambiguous_class_name;
2526
mod ambiguous_function_name;
@@ -43,3 +44,4 @@ mod tab_indentation;
4344
mod too_many_newlines_at_end_of_file;
4445
mod trailing_whitespace;
4546
mod type_comparison;
47+
mod whitespace_after_decorator;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast::Decorator;
4+
use ruff_python_trivia::is_python_whitespace;
5+
use ruff_text_size::{Ranged, TextRange, TextSize};
6+
7+
use crate::checkers::ast::Checker;
8+
9+
/// ## What it does
10+
/// Checks for trailing whitespace after a decorator's opening `@`.
11+
///
12+
/// ## Why is this bad?
13+
/// Including whitespace after the `@` symbol is not compliant with
14+
/// [PEP 8].
15+
///
16+
/// ## Example
17+
///
18+
/// ```python
19+
/// @ decorator
20+
/// def func():
21+
/// pass
22+
/// ```
23+
///
24+
/// Use instead:
25+
/// ```python
26+
/// @decorator
27+
/// def func():
28+
/// pass
29+
/// ```
30+
///
31+
/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length
32+
33+
#[violation]
34+
pub struct WhitespaceAfterDecorator;
35+
36+
impl AlwaysFixableViolation for WhitespaceAfterDecorator {
37+
#[derive_message_formats]
38+
fn message(&self) -> String {
39+
format!("Whitespace after decorator")
40+
}
41+
42+
fn fix_title(&self) -> String {
43+
"Remove whitespace".to_string()
44+
}
45+
}
46+
47+
/// E204
48+
pub(crate) fn whitespace_after_decorator(checker: &mut Checker, decorator_list: &[Decorator]) {
49+
for decorator in decorator_list {
50+
let decorator_text = checker.locator().slice(decorator);
51+
52+
// Determine whether the `@` is followed by whitespace.
53+
if let Some(trailing) = decorator_text.strip_prefix('@') {
54+
// Collect the whitespace characters after the `@`.
55+
if trailing.chars().next().is_some_and(is_python_whitespace) {
56+
let end = trailing
57+
.chars()
58+
.position(|c| !(is_python_whitespace(c) || matches!(c, '\n' | '\r' | '\\')))
59+
.unwrap_or(trailing.len());
60+
61+
let start = decorator.start() + TextSize::from(1);
62+
let end = start + TextSize::try_from(end).unwrap();
63+
let range = TextRange::new(start, end);
64+
65+
let mut diagnostic = Diagnostic::new(WhitespaceAfterDecorator, range);
66+
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range)));
67+
checker.diagnostics.push(diagnostic);
68+
}
69+
}
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
3+
---
4+
E204.py:14:2: E204 [*] Whitespace after decorator
5+
|
6+
13 | # E204
7+
14 | @ foo
8+
| ^ E204
9+
15 | def baz():
10+
16 | print('baz')
11+
|
12+
= help: Remove whitespace
13+
14+
Safe fix
15+
11 11 | print('bar')
16+
12 12 |
17+
13 13 | # E204
18+
14 |-@ foo
19+
14 |+@foo
20+
15 15 | def baz():
21+
16 16 | print('baz')
22+
17 17 |
23+
24+
E204.py:25:6: E204 [*] Whitespace after decorator
25+
|
26+
24 | # E204
27+
25 | @ foo
28+
| ^ E204
29+
26 | def baz(self):
30+
27 | print('baz')
31+
|
32+
= help: Remove whitespace
33+
34+
Safe fix
35+
22 22 | print('bar')
36+
23 23 |
37+
24 24 | # E204
38+
25 |- @ foo
39+
25 |+ @foo
40+
26 26 | def baz(self):
41+
27 27 | print('baz')
42+
28 28 |
43+
44+
E204.py:31:2: E204 [*] Whitespace after decorator
45+
|
46+
30 | # E204
47+
31 | @ \
48+
| __^
49+
32 | | foo
50+
| |_^ E204
51+
33 | def baz():
52+
34 | print('baz')
53+
|
54+
= help: Remove whitespace
55+
56+
Safe fix
57+
28 28 |
58+
29 29 |
59+
30 30 | # E204
60+
31 |-@ \
61+
32 |-foo
62+
31 |+@foo
63+
33 32 | def baz():
64+
34 33 | print('baz')

ruff.schema.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/check_docs_formatted.py

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"unnecessary-class-parentheses",
8787
"unnecessary-escaped-quote",
8888
"useless-semicolon",
89+
"whitespace-after-decorator",
8990
"whitespace-after-open-bracket",
9091
"whitespace-before-close-bracket",
9192
"whitespace-before-parameters",

0 commit comments

Comments
 (0)