[`flake8-pyi`] Implement `PYI012` (#3743)

This commit is contained in:
JBLDSKY 2023-03-27 20:27:24 +02:00 committed by GitHub
parent 450c6780ff
commit 0eb5a22dd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 343 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -775,6 +775,9 @@ where
if self.settings.rules.enabled(Rule::PassStatementStubBody) { if self.settings.rules.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(self, body); 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 if self

View File

@ -569,6 +569,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Pyi, "009") => Rule::PassStatementStubBody, (Flake8Pyi, "009") => Rule::PassStatementStubBody,
(Flake8Pyi, "010") => Rule::NonEmptyStubBody, (Flake8Pyi, "010") => Rule::NonEmptyStubBody,
(Flake8Pyi, "011") => Rule::TypedArgumentDefaultInStub, (Flake8Pyi, "011") => Rule::TypedArgumentDefaultInStub,
(Flake8Pyi, "012") => Rule::PassInClassBody,
(Flake8Pyi, "014") => Rule::ArgumentDefaultInStub, (Flake8Pyi, "014") => Rule::ArgumentDefaultInStub,
(Flake8Pyi, "015") => Rule::AssignmentDefaultInStub, (Flake8Pyi, "015") => Rule::AssignmentDefaultInStub,
(Flake8Pyi, "021") => Rule::DocstringInStub, (Flake8Pyi, "021") => Rule::DocstringInStub,

View File

@ -525,6 +525,7 @@ ruff_macros::register_rules!(
rules::flake8_pyi::rules::UnprefixedTypeParam, rules::flake8_pyi::rules::UnprefixedTypeParam,
rules::flake8_pyi::rules::UnrecognizedPlatformCheck, rules::flake8_pyi::rules::UnrecognizedPlatformCheck,
rules::flake8_pyi::rules::UnrecognizedPlatformName, rules::flake8_pyi::rules::UnrecognizedPlatformName,
rules::flake8_pyi::rules::PassInClassBody,
// flake8-pytest-style // flake8-pytest-style
rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle,
rules::flake8_pytest_style::rules::PytestFixturePositionalArgs, rules::flake8_pytest_style::rules::PytestFixturePositionalArgs,

View File

@ -27,6 +27,8 @@ mod tests {
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))] #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))]
#[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))]
#[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.pyi"))] #[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.py"))]
#[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))] #[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))]
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))] #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))]

View File

@ -1,6 +1,7 @@
pub use bad_version_info_comparison::{bad_version_info_comparison, BadVersionInfoComparison}; pub use bad_version_info_comparison::{bad_version_info_comparison, BadVersionInfoComparison};
pub use docstring_in_stubs::{docstring_in_stubs, DocstringInStub}; pub use docstring_in_stubs::{docstring_in_stubs, DocstringInStub};
pub use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; 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 pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
pub use prefix_type_params::{prefix_type_params, UnprefixedTypeParam}; pub use prefix_type_params::{prefix_type_params, UnprefixedTypeParam};
pub use simple_defaults::{ pub use simple_defaults::{
@ -15,6 +16,7 @@ pub use unrecognized_platform::{
mod bad_version_info_comparison; mod bad_version_info_comparison;
mod docstring_in_stubs; mod docstring_in_stubs;
mod non_empty_stub_body; mod non_empty_stub_body;
mod pass_in_class_body;
mod pass_statement_stub_body; mod pass_statement_stub_body;
mod prefix_type_params; mod prefix_type_params;
mod simple_defaults; mod simple_defaults;

View File

@ -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);
}
}
}

View File

@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
[]

View File

@ -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
ruff.schema.json generated
View File

@ -2034,6 +2034,7 @@
"PYI01", "PYI01",
"PYI010", "PYI010",
"PYI011", "PYI011",
"PYI012",
"PYI014", "PYI014",
"PYI015", "PYI015",
"PYI02", "PYI02",