Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pycodestyle] Whitespace after decorator (E204) #12140

Merged
merged 12 commits into from
Jul 4, 2024
34 changes: 34 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pycodestyle/E204.py
Original file line number Diff line number Diff line change
@@ -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')
6 changes: 6 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnusedAsync) {
ruff::rules::unused_async(checker, function_def);
}
if checker.enabled(Rule::WhitespaceAfterDecorator) {
pycodestyle::rules::whitespace_after_decorator(checker, decorator_list);
}
}
Stmt::Return(_) => {
if checker.enabled(Rule::ReturnOutsideFunction) {
Expand Down Expand Up @@ -531,6 +534,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MetaClassABCMeta) {
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: _ }) => {
if checker.enabled(Rule::MultipleImportsOnOneLine) {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
(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, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
(Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pycodestyle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mod tests {
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
#[test_case(Rule::UselessSemicolon, Path::new("E70.py"))]
#[test_case(Rule::UselessSemicolon, Path::new("E703.ipynb"))]
#[test_case(Rule::WhitespaceAfterDecorator, Path::new("E204.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) use tab_indentation::*;
pub(crate) use too_many_newlines_at_end_of_file::*;
pub(crate) use trailing_whitespace::*;
pub(crate) use type_comparison::*;
pub(crate) use whitespace_after_decorator::*;

mod ambiguous_class_name;
mod ambiguous_function_name;
Expand All @@ -43,3 +44,4 @@ mod tab_indentation;
mod too_many_newlines_at_end_of_file;
mod trailing_whitespace;
mod type_comparison;
mod whitespace_after_decorator;
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions scripts/check_docs_formatted.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"unnecessary-class-parentheses",
"unnecessary-escaped-quote",
"useless-semicolon",
"whitespace-after-decorator",
"whitespace-after-open-bracket",
"whitespace-before-close-bracket",
"whitespace-before-parameters",
Expand Down
Loading