[flake8-pyi]: PYI009, PYI010, PYI021 (#3230)

PYI009 and PYI010 are very similar, always use `...` in function and class bodies in stubs.

PYI021 bans doc strings in stubs.

I think all of these rules should be relatively straightforward to implement auto fixes for but can do that later once we get all the other rules added.

rel: https://github.com/charliermarsh/ruff/issues/848
This commit is contained in:
Steve Dignam
2023-02-25 22:29:04 -05:00
committed by GitHub
parent 33c31cda27
commit a8a312e862
26 changed files with 345 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -485,6 +485,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(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,

View File

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

View File

@@ -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<StmtKind>) -> Diagnostic {
pub fn assert_used(stmt: &Stmt) -> Diagnostic {
Diagnostic::new(
Assert,
Range::new(stmt.location, stmt.location.with_col_offset("assert".len())),

View File

@@ -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<ArgData>, default: &Expr) -> Option<Diagnostic> {
fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option<Diagnostic> {
let string = string_literal(default).filter(|string| !string.is_empty())?;
let kwarg_name = &arg.node.arg;
if !matches_password_name(kwarg_name) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ExprKind>,
attr_expr: &Located<ExprKind>,
) {
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(),

View File

@@ -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<ExprKind>) {
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() {

View File

@@ -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<ExcepthandlerKind>) {
fn handle_except_block(checker: &mut Checker, handler: &Excepthandler) {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
let Some(error_handlers) = type_.as_ref() else {
return;

5
ruff.schema.json generated
View File

@@ -1912,6 +1912,11 @@
"PYI001",
"PYI007",
"PYI008",
"PYI009",
"PYI01",
"PYI010",
"PYI02",
"PYI021",
"Q",
"Q0",
"Q00",