diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI048.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI048.py new file mode 100644 index 0000000000..8ec21f2d31 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI048.py @@ -0,0 +1,19 @@ +def bar(): # OK + ... + + +def oof(): # OK, docstrings are handled by another rule + """oof""" + print("foo") + + +def foo(): # Ok not in Stub file + """foo""" + print("foo") + print("foo") + + +def buzz(): # Ok not in Stub file + print("fizz") + print("buzz") + print("test") diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI048.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI048.pyi new file mode 100644 index 0000000000..29a2120f94 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI048.pyi @@ -0,0 +1,20 @@ +def bar(): + ... # OK + + +def oof(): # OK, docstrings are handled by another rule + """oof""" + print("foo") + + + +def foo(): # ERROR PYI048 + """foo""" + print("foo") + print("foo") + + +def buzz(): # ERROR PYI048 + print("fizz") + print("buzz") + print("test") diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index bff12fb28e..1028f33de4 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -425,6 +425,9 @@ where if self.enabled(Rule::NonEmptyStubBody) { flake8_pyi::rules::non_empty_stub_body(self, body); } + if self.enabled(Rule::StubBodyMultipleStatements) { + flake8_pyi::rules::stub_body_multiple_statements(self, stmt, body); + } } if self.enabled(Rule::DunderFunctionName) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 583f51e470..2b55ab6d0e 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -592,6 +592,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "033") => (RuleGroup::Unspecified, Rule::TypeCommentInStub), (Flake8Pyi, "042") => (RuleGroup::Unspecified, Rule::SnakeCaseTypeAlias), (Flake8Pyi, "043") => (RuleGroup::Unspecified, Rule::TSuffixedTypeAlias), + (Flake8Pyi, "048") => (RuleGroup::Unspecified, Rule::StubBodyMultipleStatements), (Flake8Pyi, "052") => (RuleGroup::Unspecified, Rule::UnannotatedAssignmentInStub), // flake8-pytest-style diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index d0aae92083..1f975cff38 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -519,6 +519,7 @@ ruff_macros::register_rules!( rules::flake8_pyi::rules::PassStatementStubBody, rules::flake8_pyi::rules::QuotedAnnotationInStub, rules::flake8_pyi::rules::SnakeCaseTypeAlias, + rules::flake8_pyi::rules::StubBodyMultipleStatements, rules::flake8_pyi::rules::TSuffixedTypeAlias, rules::flake8_pyi::rules::TypeCommentInStub, rules::flake8_pyi::rules::TypedArgumentDefaultInStub, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 436c370369..ad62a00f86 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -34,6 +34,8 @@ mod tests { #[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))] + #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.py"))] + #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 14f00d1d24..0c6b55d634 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -16,6 +16,9 @@ pub(crate) use simple_defaults::{ typed_argument_simple_defaults, unannotated_assignment_in_stub, ArgumentDefaultInStub, AssignmentDefaultInStub, TypedArgumentDefaultInStub, UnannotatedAssignmentInStub, }; +pub(crate) use stub_body_multiple_statements::{ + stub_body_multiple_statements, StubBodyMultipleStatements, +}; pub(crate) use type_alias_naming::{ snake_case_type_alias, t_suffixed_type_alias, SnakeCaseTypeAlias, TSuffixedTypeAlias, }; @@ -34,6 +37,7 @@ mod pass_statement_stub_body; mod prefix_type_params; mod quoted_annotation_in_stub; mod simple_defaults; +mod stub_body_multiple_statements; mod type_alias_naming; mod type_comment_in_stub; mod unrecognized_platform; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs new file mode 100644 index 0000000000..3ab3613237 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs @@ -0,0 +1,37 @@ +use rustpython_parser::ast::Stmt; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers; +use ruff_python_ast::helpers::is_docstring_stmt; + +use crate::checkers::ast::Checker; + +#[violation] +pub struct StubBodyMultipleStatements; + +impl Violation for StubBodyMultipleStatements { + #[derive_message_formats] + fn message(&self) -> String { + format!("Function body must contain exactly one statement") + } +} + +/// PYI010 +pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, body: &[Stmt]) { + // If the function body consists of exactly one statement, abort. + if body.len() == 1 { + return; + } + + // If the function body consists of exactly two statements, and the first is a + // docstring, abort (this is covered by PYI021). + if body.len() == 2 && is_docstring_stmt(&body[0]) { + return; + } + + checker.diagnostics.push(Diagnostic::new( + StubBodyMultipleStatements, + helpers::identifier_range(stmt, checker.locator), + )); +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI048_PYI048.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI048_PYI048.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI048_PYI048.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI048_PYI048.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI048_PYI048.pyi.snap new file mode 100644 index 0000000000..725d9b7b4d --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI048_PYI048.pyi.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI048.pyi:11:5: PYI048 Function body must contain exactly one statement + | +11 | def foo(): # ERROR PYI048 + | ^^^ PYI048 +12 | """foo""" +13 | print("foo") + | + +PYI048.pyi:17:5: PYI048 Function body must contain exactly one statement + | +17 | def buzz(): # ERROR PYI048 + | ^^^^ PYI048 +18 | print("fizz") +19 | print("buzz") + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 0a47edb5c9..5bf96af3b8 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2206,6 +2206,7 @@ "PYI04", "PYI042", "PYI043", + "PYI048", "PYI05", "PYI052", "Q",