Skip to content

Commit

Permalink
[flake8-pyi] Implement PYI012 (astral-sh#3743)
Browse files Browse the repository at this point in the history
  • Loading branch information
JBLDKY authored Mar 27, 2023
1 parent 450c678 commit 0eb5a22
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 0 deletions.
75 changes: 75 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Violations of PYI012


class OneAttributeClass:
value: int
pass # PYI012 Class body must not contain `pass`


class OneAttributeClassRev:
pass # PYI012 Class body must not contain `pass`
value: int


class DocstringClass:
"""
My body only contains pass.
"""

pass # PYI012 Class body must not contain `pass`


class NonEmptyChild(Exception):
value: int
pass # PYI012 Class body must not contain `pass`


class NonEmptyChild2(Exception):
pass # PYI012 Class body must not contain `pass`
value: int


class NonEmptyWithInit:
value: int
pass # PYI012 Class body must not contain `pass`

def __init__():
pass


# Not violations (of PYI012)


class EmptyClass:
pass # Y009 Empty body should contain `...`, not `pass`


class EmptyOneLine:
pass # Y009 Empty body should contain `...`, not `pass`


class Dog:
eyes: int = 2


class EmptyEllipsis:
...


class NonEmptyEllipsis:
value: int
... # Y013 Non-empty class body must not contain `...`


class WithInit:
value: int = 0

def __init__():
pass


def function():
pass


pass
59 changes: 59 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Violations of PYI012

class OneAttributeClass:
value: int
pass # PYI012 Class body must not contain `pass`

class OneAttributeClassRev:
pass # PYI012 Class body must not contain `pass`
value: int

class DocstringClass:
"""
My body only contains pass.
"""

pass # PYI012 Class body must not contain `pass`

class NonEmptyChild(Exception):
value: int
pass # PYI012 Class body must not contain `pass`

class NonEmptyChild2(Exception):
pass # PYI012 Class body must not contain `pass`
value: int

class NonEmptyWithInit:
value: int
pass # PYI012 Class body must not contain `pass`

def __init__():
pass

# Not violations (of PYI012)

class EmptyClass:
pass # Y009 Empty body should contain `...`, not `pass`

class EmptyOneLine:
pass # Y009 Empty body should contain `...`, not `pass`

class Dog:
eyes: int = 2

class EmptyEllipsis: ...

class NonEmptyEllipsis:
value: int
... # Y013 Non-empty class body must not contain `...`

class WithInit:
value: int = 0

def __init__():
pass

def function():
pass

pass
3 changes: 3 additions & 0 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,9 @@ where
if self.settings.rules.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(self, body);
}
if self.settings.rules.enabled(Rule::PassInClassBody) {
flake8_pyi::rules::pass_in_class_body(self, stmt, body);
}
}

if self
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Pyi, "009") => Rule::PassStatementStubBody,
(Flake8Pyi, "010") => Rule::NonEmptyStubBody,
(Flake8Pyi, "011") => Rule::TypedArgumentDefaultInStub,
(Flake8Pyi, "012") => Rule::PassInClassBody,
(Flake8Pyi, "014") => Rule::ArgumentDefaultInStub,
(Flake8Pyi, "015") => Rule::AssignmentDefaultInStub,
(Flake8Pyi, "021") => Rule::DocstringInStub,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ ruff_macros::register_rules!(
rules::flake8_pyi::rules::UnprefixedTypeParam,
rules::flake8_pyi::rules::UnrecognizedPlatformCheck,
rules::flake8_pyi::rules::UnrecognizedPlatformName,
rules::flake8_pyi::rules::PassInClassBody,
// flake8-pytest-style
rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle,
rules::flake8_pytest_style::rules::PytestFixturePositionalArgs,
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/flake8_pyi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ mod tests {
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))]
#[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))]
#[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.pyi"))]
#[test_case(Rule::PassInClassBody, Path::new("PYI012.py"))]
#[test_case(Rule::PassInClassBody, Path::new("PYI012.pyi"))]
#[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.py"))]
#[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))]
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))]
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/flake8_pyi/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub use bad_version_info_comparison::{bad_version_info_comparison, BadVersionInfoComparison};
pub use docstring_in_stubs::{docstring_in_stubs, DocstringInStub};
pub use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody};
pub use pass_in_class_body::{pass_in_class_body, PassInClassBody};
pub use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
pub use prefix_type_params::{prefix_type_params, UnprefixedTypeParam};
pub use simple_defaults::{
Expand All @@ -15,6 +16,7 @@ pub use unrecognized_platform::{
mod bad_version_info_comparison;
mod docstring_in_stubs;
mod non_empty_stub_body;
mod pass_in_class_body;
mod pass_statement_stub_body;
mod prefix_type_params;
mod simple_defaults;
Expand Down
62 changes: 62 additions & 0 deletions crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use crate::autofix::helpers::delete_stmt;
use log::error;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::{Range, RefEquality};

use crate::checkers::ast::Checker;

use crate::registry::AsRule;
use rustpython_parser::ast::{Stmt, StmtKind};

#[violation]
pub struct PassInClassBody;

impl AlwaysAutofixableViolation for PassInClassBody {
#[derive_message_formats]
fn message(&self) -> String {
format!("Class body must not contain `pass`")
}

fn autofix_title(&self) -> String {
format!("Remove unnecessary `pass`")
}
}

/// PYI012
pub fn pass_in_class_body<'a>(checker: &mut Checker<'a>, parent: &'a Stmt, body: &'a [Stmt]) {
// `pass` is required in these situations (or handled by `pass_statement_stub_body`).
if body.len() < 2 {
return;
}

for stmt in body {
if matches!(stmt.node, StmtKind::Pass) {
let mut diagnostic = Diagnostic::new(PassInClassBody, Range::from(stmt));

if checker.patch(diagnostic.kind.rule()) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
match delete_stmt(
stmt,
Some(parent),
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
checker.deletions.insert(RefEquality(stmt));
}
diagnostic.set_fix(fix);
}
Err(e) => {
error!("Failed to delete `pass` statement: {}", e);
}
};
};

checker.diagnostics.push(diagnostic);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
[]

Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
- kind:
name: PassInClassBody
body: "Class body must not contain `pass`"
suggestion: "Remove unnecessary `pass`"
fixable: true
location:
row: 5
column: 4
end_location:
row: 5
column: 8
fix:
edits:
- content: ""
location:
row: 5
column: 0
end_location:
row: 6
column: 0
parent: ~
- kind:
name: PassInClassBody
body: "Class body must not contain `pass`"
suggestion: "Remove unnecessary `pass`"
fixable: true
location:
row: 8
column: 4
end_location:
row: 8
column: 8
fix:
edits:
- content: ""
location:
row: 8
column: 0
end_location:
row: 9
column: 0
parent: ~
- kind:
name: PassInClassBody
body: "Class body must not contain `pass`"
suggestion: "Remove unnecessary `pass`"
fixable: true
location:
row: 16
column: 4
end_location:
row: 16
column: 8
fix:
edits:
- content: ""
location:
row: 16
column: 0
end_location:
row: 17
column: 0
parent: ~
- kind:
name: PassInClassBody
body: "Class body must not contain `pass`"
suggestion: "Remove unnecessary `pass`"
fixable: true
location:
row: 20
column: 4
end_location:
row: 20
column: 8
fix:
edits:
- content: ""
location:
row: 20
column: 0
end_location:
row: 21
column: 0
parent: ~
- kind:
name: PassInClassBody
body: "Class body must not contain `pass`"
suggestion: "Remove unnecessary `pass`"
fixable: true
location:
row: 23
column: 4
end_location:
row: 23
column: 8
fix:
edits:
- content: ""
location:
row: 23
column: 0
end_location:
row: 24
column: 0
parent: ~
- kind:
name: PassInClassBody
body: "Class body must not contain `pass`"
suggestion: "Remove unnecessary `pass`"
fixable: true
location:
row: 28
column: 4
end_location:
row: 28
column: 8
fix:
edits:
- content: ""
location:
row: 28
column: 0
end_location:
row: 29
column: 0
parent: ~

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.

0 comments on commit 0eb5a22

Please sign in to comment.