[`flake8-pyi`] Implement `PYI013` (#4517)

This commit is contained in:
Justin Prieto 2023-05-19 11:39:55 -04:00 committed by GitHub
parent 7ebe372122
commit 837e70677b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 411 additions and 0 deletions

View File

@ -0,0 +1,65 @@
class OneAttributeClass:
value: int
...
class OneAttributeClass2:
...
value: int
class TwoEllipsesClass:
...
...
class DocstringClass:
"""
My body only contains an ellipsis.
"""
...
class NonEmptyChild(Exception):
value: int
...
class NonEmptyChild2(Exception):
...
value: int
class NonEmptyWithInit:
value: int
...
def __init__():
pass
class EmptyClass:
...
class EmptyEllipsis:
...
class Dog:
eyes: int = 2
class WithInit:
value: int = 0
def __init__():
...
def function():
...
...

View File

@ -0,0 +1,56 @@
# Violations of PYI013
class OneAttributeClass:
value: int
... # Error
class OneAttributeClass2:
... # Error
value: int
class MyClass:
...
value: int
class TwoEllipsesClass:
...
... # Error
class DocstringClass:
"""
My body only contains an ellipsis.
"""
... # Error
class NonEmptyChild(Exception):
value: int
... # Error
class NonEmptyChild2(Exception):
... # Error
value: int
class NonEmptyWithInit:
value: int
... # Error
def __init__():
pass
# Not violations
class EmptyClass: ...
class EmptyEllipsis: ...
class Dog:
eyes: int = 2
class WithInit:
value: int = 0
def __init__(): ...
def function(): ...
...

View File

@ -742,6 +742,13 @@ where
if self.settings.rules.enabled(Rule::PassInClassBody) {
flake8_pyi::rules::pass_in_class_body(self, stmt, body);
}
if self
.settings
.rules
.enabled(Rule::EllipsisInNonEmptyClassBody)
{
flake8_pyi::rules::ellipsis_in_non_empty_class_body(self, stmt, body);
}
}
if self

View File

@ -582,6 +582,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "010") => (RuleGroup::Unspecified, Rule::NonEmptyStubBody),
(Flake8Pyi, "011") => (RuleGroup::Unspecified, Rule::TypedArgumentDefaultInStub),
(Flake8Pyi, "012") => (RuleGroup::Unspecified, Rule::PassInClassBody),
(Flake8Pyi, "013") => (RuleGroup::Unspecified, Rule::EllipsisInNonEmptyClassBody),
(Flake8Pyi, "014") => (RuleGroup::Unspecified, Rule::ArgumentDefaultInStub),
(Flake8Pyi, "015") => (RuleGroup::Unspecified, Rule::AssignmentDefaultInStub),
(Flake8Pyi, "016") => (RuleGroup::Unspecified, Rule::DuplicateUnionMember),

View File

@ -512,6 +512,7 @@ ruff_macros::register_rules!(
rules::flake8_pyi::rules::BadVersionInfoComparison,
rules::flake8_pyi::rules::DocstringInStub,
rules::flake8_pyi::rules::DuplicateUnionMember,
rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody,
rules::flake8_pyi::rules::NonEmptyStubBody,
rules::flake8_pyi::rules::PassInClassBody,
rules::flake8_pyi::rules::PassStatementStubBody,

View File

@ -23,6 +23,8 @@ mod tests {
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))]
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.py"))]
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.pyi"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))]
#[test_case(Rule::PassInClassBody, Path::new("PYI012.py"))]

View File

@ -0,0 +1,93 @@
use rustpython_parser::ast::{Expr, ExprConstant, Ranged, Stmt, StmtExpr};
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix::actions::delete_stmt;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Removes ellipses (`...`) in otherwise non-empty class bodies.
///
/// ## Why is this bad?
/// An ellipsis in a class body is only necessary if the class body is
/// otherwise empty. If the class body is non-empty, then the ellipsis
/// is redundant.
///
/// ## Example
/// ```python
/// class Foo:
/// ...
/// value: int
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// value: int
/// ```
#[violation]
pub struct EllipsisInNonEmptyClassBody;
impl Violation for EllipsisInNonEmptyClassBody {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Non-empty class body must not contain `...`")
}
fn autofix_title(&self) -> Option<String> {
Some("Remove unnecessary `...`".to_string())
}
}
/// PYI013
pub(crate) fn ellipsis_in_non_empty_class_body<'a>(
checker: &mut Checker<'a>,
parent: &'a Stmt,
body: &'a [Stmt],
) {
// If the class body contains a single statement, then it's fine for it to be an ellipsis.
if body.len() == 1 {
return;
}
for stmt in body {
if let Stmt::Expr(StmtExpr { value, .. }) = &stmt {
if let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() {
if value.is_ellipsis() {
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let deleted: Vec<&Stmt> =
checker.deletions.iter().map(Into::into).collect();
let edit = delete_stmt(
stmt,
Some(parent),
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
)?;
// In the unlikely event the class body consists solely of several
// consecutive ellipses, `delete_stmt` can actually result in a
// `pass`.
if edit.is_deletion() || edit.content() == Some("pass") {
checker.deletions.insert(RefEquality(stmt));
}
Ok(Fix::automatic(edit))
});
}
checker.diagnostics.push(diagnostic);
}
}
}
}
}

View File

@ -3,6 +3,9 @@ pub(crate) use bad_version_info_comparison::{
};
pub(crate) use docstring_in_stubs::{docstring_in_stubs, DocstringInStub};
pub(crate) use duplicate_union_member::{duplicate_union_member, DuplicateUnionMember};
pub(crate) use ellipsis_in_non_empty_class_body::{
ellipsis_in_non_empty_class_body, EllipsisInNonEmptyClassBody,
};
pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody};
pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody};
pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
@ -24,6 +27,7 @@ pub(crate) use unrecognized_platform::{
mod bad_version_info_comparison;
mod docstring_in_stubs;
mod duplicate_union_member;
mod ellipsis_in_non_empty_class_body;
mod non_empty_stub_body;
mod pass_in_class_body;
mod pass_statement_stub_body;

View File

@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

View File

@ -0,0 +1,177 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...`
|
5 | class OneAttributeClass:
6 | value: int
7 | ... # Error
| ^^^ PYI013
8 |
9 | class OneAttributeClass2:
|
= help: Remove unnecessary `...`
Fix
2 2 |
3 3 | class OneAttributeClass:
4 4 | value: int
5 |- ... # Error
6 5 |
7 6 | class OneAttributeClass2:
8 7 | ... # Error
PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
|
8 | class OneAttributeClass2:
9 | ... # Error
| ^^^ PYI013
10 | value: int
|
= help: Remove unnecessary `...`
Fix
5 5 | ... # Error
6 6 |
7 7 | class OneAttributeClass2:
8 |- ... # Error
9 8 | value: int
10 9 |
11 10 | class MyClass:
PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...`
|
12 | class MyClass:
13 | ...
| ^^^ PYI013
14 | value: int
|
= help: Remove unnecessary `...`
Fix
9 9 | value: int
10 10 |
11 11 | class MyClass:
12 |- ...
13 12 | value: int
14 13 |
15 14 | class TwoEllipsesClass:
PYI013.pyi:16:5: PYI013 [*] Non-empty class body must not contain `...`
|
16 | class TwoEllipsesClass:
17 | ...
| ^^^ PYI013
18 | ... # Error
|
= help: Remove unnecessary `...`
Fix
13 13 | value: int
14 14 |
15 15 | class TwoEllipsesClass:
16 |- ...
17 16 | ... # Error
18 17 |
19 18 | class DocstringClass:
PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
|
17 | class TwoEllipsesClass:
18 | ...
19 | ... # Error
| ^^^ PYI013
20 |
21 | class DocstringClass:
|
= help: Remove unnecessary `...`
Fix
14 14 |
15 15 | class TwoEllipsesClass:
16 16 | ...
17 |- ... # Error
17 |+ pass # Error
18 18 |
19 19 | class DocstringClass:
20 20 | """
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|
24 | """
25 |
26 | ... # Error
| ^^^ PYI013
27 |
28 | class NonEmptyChild(Exception):
|
= help: Remove unnecessary `...`
Fix
21 21 | My body only contains an ellipsis.
22 22 | """
23 23 |
24 |- ... # Error
25 24 |
26 25 | class NonEmptyChild(Exception):
27 26 | value: int
PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
|
28 | class NonEmptyChild(Exception):
29 | value: int
30 | ... # Error
| ^^^ PYI013
31 |
32 | class NonEmptyChild2(Exception):
|
= help: Remove unnecessary `...`
Fix
25 25 |
26 26 | class NonEmptyChild(Exception):
27 27 | value: int
28 |- ... # Error
29 28 |
30 29 | class NonEmptyChild2(Exception):
31 30 | ... # Error
PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
|
31 | class NonEmptyChild2(Exception):
32 | ... # Error
| ^^^ PYI013
33 | value: int
|
= help: Remove unnecessary `...`
Fix
28 28 | ... # Error
29 29 |
30 30 | class NonEmptyChild2(Exception):
31 |- ... # Error
32 31 | value: int
33 32 |
34 33 | class NonEmptyWithInit:
PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
36 | class NonEmptyWithInit:
37 | value: int
38 | ... # Error
| ^^^ PYI013
39 |
40 | def __init__():
|
= help: Remove unnecessary `...`
Fix
33 33 |
34 34 | class NonEmptyWithInit:
35 35 | value: int
36 |- ... # Error
37 36 |
38 37 | def __init__():
39 38 | pass

1
ruff.schema.json generated
View File

@ -2159,6 +2159,7 @@
"PYI010",
"PYI011",
"PYI012",
"PYI013",
"PYI014",
"PYI015",
"PYI016",