diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI009.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI009.py new file mode 100644 index 0000000000..b5f5b3780c --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI009.py @@ -0,0 +1,14 @@ +def bar(): + ... # OK + + +def foo(): + pass # OK, since we're not in a stub file + + +class Bar: + ... # OK + + +class Foo: + pass # OK, since we're not in a stub file diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI009.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI009.pyi new file mode 100644 index 0000000000..de88fbfa0d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI009.pyi @@ -0,0 +1,8 @@ +def bar(): ... # OK +def foo(): + pass # ERROR PYI009, since we're in a stub file + +class Bar: ... # OK + +class Foo: + pass # ERROR PYI009, since we're in a stub file diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI010.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI010.py new file mode 100644 index 0000000000..1c6afa31ad --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI010.py @@ -0,0 +1,18 @@ +def bar(): + ... # OK + + +def foo(): + """foo""" # OK + + +def buzz(): + print("buzz") # OK, not in stub file + + +def foo2(): + 123 # OK, not in a stub file + + +def bizz(): + x = 123 # OK, not in a stub file diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI010.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI010.pyi new file mode 100644 index 0000000000..b49859d824 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI010.pyi @@ -0,0 +1,12 @@ +def bar(): ... # OK +def foo(): + """foo""" # OK, strings are handled by another rule + +def buzz(): + print("buzz") # ERROR PYI010 + +def foo2(): + 123 # ERROR PYI010 + +def bizz(): + x = 123 # ERROR PYI010 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI021.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI021.py new file mode 100644 index 0000000000..d2b1f45616 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI021.py @@ -0,0 +1,14 @@ +"""foo""" # OK, not in stub + + +def foo(): + """foo""" # OK, doc strings are allowed in non-stubs + + +class Bar: + """bar""" # OK, doc strings are allowed in non-stubs + + +def bar(): + x = 1 + """foo""" # OK, not a doc string diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI021.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI021.pyi new file mode 100644 index 0000000000..f28a4cb9b1 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI021.pyi @@ -0,0 +1,11 @@ +"""foo""" # ERROR PYI021 + +def foo(): + """foo""" # ERROR PYI021 + +class Bar: + """bar""" # ERROR PYI021 + +def bar(): + x = 1 + """foo""" # OK, not a doc string diff --git a/crates/ruff/src/checkers/ast.rs b/crates/ruff/src/checkers/ast.rs index 6ef66bb48f..0d25adf49b 100644 --- a/crates/ruff/src/checkers/ast.rs +++ b/crates/ruff/src/checkers/ast.rs @@ -536,6 +536,15 @@ where } } + if self.is_interface_definition { + if self.settings.rules.enabled(&Rule::PassStatementStubBody) { + flake8_pyi::rules::pass_statement_stub_body(self, body); + } + if self.settings.rules.enabled(&Rule::NonEmptyStubBody) { + flake8_pyi::rules::non_empty_stub_body(self, body); + } + } + if self.settings.rules.enabled(&Rule::DunderFunctionName) { if let Some(diagnostic) = pep8_naming::rules::dunder_function_name( self.current_scope(), @@ -864,6 +873,11 @@ where ); } } + if self.is_interface_definition { + if self.settings.rules.enabled(&Rule::PassStatementStubBody) { + flake8_pyi::rules::pass_statement_stub_body(self, body); + } + } if self .settings @@ -5346,6 +5360,11 @@ impl<'a> Checker<'a> { } overloaded_name = flake8_annotations::helpers::overloaded_name(self, &definition); } + if self.is_interface_definition { + if self.settings.rules.enabled(&Rule::DocstringInStub) { + flake8_pyi::rules::docstring_in_stubs(self, definition.docstring); + } + } // pydocstyle if enforce_docstrings { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index d575a20af1..3ffe6f83e1 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -485,6 +485,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Pyi, "001") => Rule::PrefixTypeParams, (Flake8Pyi, "007") => Rule::UnrecognizedPlatformCheck, (Flake8Pyi, "008") => Rule::UnrecognizedPlatformName, + (Flake8Pyi, "009") => Rule::PassStatementStubBody, + (Flake8Pyi, "010") => Rule::NonEmptyStubBody, + (Flake8Pyi, "021") => Rule::DocstringInStub, // flake8-pytest-style (Flake8PytestStyle, "001") => Rule::IncorrectFixtureParenthesesStyle, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index ebdca77595..ca6dbdbe7b 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -459,6 +459,9 @@ ruff_macros::register_rules!( rules::flake8_pyi::rules::PrefixTypeParams, rules::flake8_pyi::rules::UnrecognizedPlatformCheck, rules::flake8_pyi::rules::UnrecognizedPlatformName, + rules::flake8_pyi::rules::PassStatementStubBody, + rules::flake8_pyi::rules::NonEmptyStubBody, + rules::flake8_pyi::rules::DocstringInStub, // flake8-pytest-style rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle, rules::flake8_pytest_style::rules::FixturePositionalArgs, diff --git a/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs b/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs index 0c6df63446..a1b254eab3 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/assert_used.rs @@ -1,5 +1,6 @@ +use rustpython_parser::ast::Stmt; + use ruff_macros::{define_violation, derive_message_formats}; -use rustpython_parser::ast::{Located, StmtKind}; use crate::ast::types::Range; use crate::registry::Diagnostic; @@ -16,7 +17,7 @@ impl Violation for Assert { } /// S101 -pub fn assert_used(stmt: &Located) -> Diagnostic { +pub fn assert_used(stmt: &Stmt) -> Diagnostic { Diagnostic::new( Assert, Range::new(stmt.location, stmt.location.with_col_offset("assert".len())), diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs index 7aa86b4eb5..001994c931 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs @@ -1,11 +1,13 @@ -use ruff_macros::{define_violation, derive_message_formats}; -use rustpython_parser::ast::{ArgData, Arguments, Expr, Located}; +use rustpython_parser::ast::{Arg, Arguments, Expr}; + +use ruff_macros::{define_violation, derive_message_formats}; -use super::super::helpers::{matches_password_name, string_literal}; use crate::ast::types::Range; use crate::registry::Diagnostic; use crate::violation::Violation; +use super::super::helpers::{matches_password_name, string_literal}; + define_violation!( pub struct HardcodedPasswordDefault { pub string: String, @@ -19,7 +21,7 @@ impl Violation for HardcodedPasswordDefault { } } -fn check_password_kwarg(arg: &Located, default: &Expr) -> Option { +fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option { let string = string_literal(default).filter(|string| !string.is_empty())?; let kwarg_name = &arg.node.arg; if !matches_password_name(kwarg_name) { diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index b46ad749e6..7e7995b59b 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -19,6 +19,12 @@ mod tests { #[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.py"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))] + #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))] + #[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))] + #[test_case(Rule::PassStatementStubBody, Path::new("PYI009.py"))] + #[test_case(Rule::PassStatementStubBody, Path::new("PYI009.pyi"))] + #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] + #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/flake8_pyi/rules/docstring_in_stubs.rs b/crates/ruff/src/rules/flake8_pyi/rules/docstring_in_stubs.rs new file mode 100644 index 0000000000..7265f585f3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/docstring_in_stubs.rs @@ -0,0 +1,28 @@ +use rustpython_parser::ast::Expr; + +use ruff_macros::{define_violation, derive_message_formats}; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::registry::Diagnostic; +use crate::violation::Violation; + +define_violation!( + pub struct DocstringInStub; +); +impl Violation for DocstringInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!("Docstrings should not be included in stubs") + } +} + +/// PYI021 +pub fn docstring_in_stubs(checker: &mut Checker, docstring: Option<&Expr>) { + if let Some(docstr) = &docstring { + checker.diagnostics.push(Diagnostic::new( + DocstringInStub, + Range::from_located(docstr), + )); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 8939ef7a80..7482ee3529 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -1,7 +1,13 @@ +pub use docstring_in_stubs::{docstring_in_stubs, DocstringInStub}; +pub use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; +pub use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody}; pub use prefix_type_params::{prefix_type_params, PrefixTypeParams}; pub use unrecognized_platform::{ unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName, }; +mod docstring_in_stubs; +mod non_empty_stub_body; +mod pass_statement_stub_body; mod prefix_type_params; mod unrecognized_platform; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_empty_stub_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_empty_stub_body.rs new file mode 100644 index 0000000000..2a064a9387 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_empty_stub_body.rs @@ -0,0 +1,36 @@ +use rustpython_parser::ast::{Constant, ExprKind, Stmt, StmtKind}; + +use ruff_macros::{define_violation, derive_message_formats}; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::registry::Diagnostic; +use crate::violation::Violation; + +define_violation!( + pub struct NonEmptyStubBody; +); +impl Violation for NonEmptyStubBody { + #[derive_message_formats] + fn message(&self) -> String { + format!("Function body must contain only `...`") + } +} + +/// PYI010 +pub fn non_empty_stub_body(checker: &mut Checker, body: &[Stmt]) { + if body.len() != 1 { + return; + } + if let StmtKind::Expr { value } = &body[0].node { + if let ExprKind::Constant { value, .. } = &value.node { + if matches!(value, Constant::Ellipsis | Constant::Str(_)) { + return; + } + } + } + checker.diagnostics.push(Diagnostic::new( + NonEmptyStubBody, + Range::from_located(&body[0]), + )); +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs new file mode 100644 index 0000000000..31693ae314 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs @@ -0,0 +1,31 @@ +use rustpython_parser::ast::{Stmt, StmtKind}; + +use ruff_macros::{define_violation, derive_message_formats}; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::registry::Diagnostic; +use crate::violation::Violation; + +define_violation!( + pub struct PassStatementStubBody; +); +impl Violation for PassStatementStubBody { + #[derive_message_formats] + fn message(&self) -> String { + format!("Empty body should contain `...`, not `pass`") + } +} + +/// PYI009 +pub fn pass_statement_stub_body(checker: &mut Checker, body: &[Stmt]) { + if body.len() != 1 { + return; + } + if matches!(body[0].node, StmtKind::Pass) { + checker.diagnostics.push(Diagnostic::new( + PassStatementStubBody, + Range::from_located(&body[0]), + )); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI009_PYI009.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI009_PYI009.py.snap new file mode 100644 index 0000000000..efcc2d0c99 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI009_PYI009.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__PYI009_PYI009.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI009_PYI009.pyi.snap new file mode 100644 index 0000000000..fa5be660aa --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI009_PYI009.pyi.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + PassStatementStubBody: ~ + location: + row: 3 + column: 4 + end_location: + row: 3 + column: 8 + fix: ~ + parent: ~ +- kind: + PassStatementStubBody: ~ + location: + row: 8 + column: 4 + end_location: + row: 8 + column: 8 + fix: ~ + parent: ~ + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI010_PYI010.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI010_PYI010.py.snap new file mode 100644 index 0000000000..efcc2d0c99 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI010_PYI010.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__PYI010_PYI010.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI010_PYI010.pyi.snap new file mode 100644 index 0000000000..5f2d1566ee --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI010_PYI010.pyi.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + NonEmptyStubBody: ~ + location: + row: 6 + column: 4 + end_location: + row: 6 + column: 17 + fix: ~ + parent: ~ +- kind: + NonEmptyStubBody: ~ + location: + row: 9 + column: 4 + end_location: + row: 9 + column: 7 + fix: ~ + parent: ~ +- kind: + NonEmptyStubBody: ~ + location: + row: 12 + column: 4 + end_location: + row: 12 + column: 11 + fix: ~ + parent: ~ + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI021_PYI021.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI021_PYI021.py.snap new file mode 100644 index 0000000000..efcc2d0c99 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI021_PYI021.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__PYI021_PYI021.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI021_PYI021.pyi.snap new file mode 100644 index 0000000000..1eb3a86645 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI021_PYI021.pyi.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + DocstringInStub: ~ + location: + row: 1 + column: 0 + end_location: + row: 1 + column: 9 + fix: ~ + parent: ~ +- kind: + DocstringInStub: ~ + location: + row: 4 + column: 4 + end_location: + row: 4 + column: 13 + fix: ~ + parent: ~ +- kind: + DocstringInStub: ~ + location: + row: 7 + column: 4 + end_location: + row: 7 + column: 13 + fix: ~ + parent: ~ + diff --git a/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs b/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs index 33c394971a..be0461dd76 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/check_attr.rs @@ -1,5 +1,6 @@ +use rustpython_parser::ast::{Expr, ExprKind}; + use ruff_macros::{define_violation, derive_message_formats}; -use rustpython_parser::ast::{ExprKind, Located}; use crate::ast::types::{BindingKind, Range}; use crate::checkers::ast::Checker; @@ -47,12 +48,7 @@ impl Violation for UseOfDotValues { } } -pub fn check_attr( - checker: &mut Checker, - attr: &str, - value: &Located, - attr_expr: &Located, -) { +pub fn check_attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: &Expr) { let rules = &checker.settings.rules; let violation: DiagnosticKind = match attr { "ix" if rules.enabled(&Rule::UseOfDotIx) => UseOfDotIx.into(), diff --git a/crates/ruff/src/rules/pandas_vet/rules/check_call.rs b/crates/ruff/src/rules/pandas_vet/rules/check_call.rs index dacd841d99..52cf6cb784 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/check_call.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/check_call.rs @@ -1,5 +1,6 @@ +use rustpython_parser::ast::{Expr, ExprKind}; + use ruff_macros::{define_violation, derive_message_formats}; -use rustpython_parser::ast::{ExprKind, Located}; use crate::ast::types::{BindingKind, Range}; use crate::checkers::ast::Checker; @@ -59,7 +60,7 @@ impl Violation for UseOfDotStack { } } -pub fn check_call(checker: &mut Checker, func: &Located) { +pub fn check_call(checker: &mut Checker, func: &Expr) { let rules = &checker.settings.rules; let ExprKind::Attribute { value, attr, .. } = &func.node else {return}; let violation: DiagnosticKind = match attr.as_str() { diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 5bf9f7b14d..376cb4f92f 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -1,6 +1,7 @@ use itertools::Itertools; +use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind}; + use ruff_macros::{define_violation, derive_message_formats}; -use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Located}; use crate::ast::helpers::compose_call_path; use crate::ast::types::Range; @@ -97,7 +98,7 @@ fn handle_name_or_attribute( } /// Handles one block of an except (use a loop if there are multiple blocks) -fn handle_except_block(checker: &mut Checker, handler: &Located) { +fn handle_except_block(checker: &mut Checker, handler: &Excepthandler) { let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node; let Some(error_handlers) = type_.as_ref() else { return; diff --git a/ruff.schema.json b/ruff.schema.json index d5d64083f9..2ca3be755d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1912,6 +1912,11 @@ "PYI001", "PYI007", "PYI008", + "PYI009", + "PYI01", + "PYI010", + "PYI02", + "PYI021", "Q", "Q0", "Q00",