diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.py new file mode 100644 index 0000000000..72c91bdecf --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.py @@ -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 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.pyi new file mode 100644 index 0000000000..9c19a2d6d7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI012.pyi @@ -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 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 2aad233b7e..6a70f6747b 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -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 diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 12b4b8ece5..ff0c93a021 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -569,6 +569,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (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, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 01ed34da9a..1a8ac17993 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -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, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index fa8696dd8d..b51394c578 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -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"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 3ad9de8513..0b9052accc 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -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::{ @@ -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; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs new file mode 100644 index 0000000000..6123a72ec0 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs @@ -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); + } + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI012_PYI012.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI012_PYI012.py.snap new file mode 100644 index 0000000000..efcc2d0c99 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI012_PYI012.py.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +[] + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI012_PYI012.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI012_PYI012.pyi.snap new file mode 100644 index 0000000000..da2b108f30 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI012_PYI012.pyi.snap @@ -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: ~ + diff --git a/ruff.schema.json b/ruff.schema.json index 38db2bd60c..6ac0c5b127 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2034,6 +2034,7 @@ "PYI01", "PYI010", "PYI011", + "PYI012", "PYI014", "PYI015", "PYI02",