-
Notifications
You must be signed in to change notification settings - Fork 888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[flake8-pyi
] Implement PYI029
#4851
Changes from 1 commit
c076960
d044437
9cdcbf1
803771a
de3debb
c639457
f336e73
a59b888
07ab4ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import builtins | ||
from abc import abstractmethod | ||
|
||
|
||
def __repr__(self) -> str: | ||
... | ||
|
||
|
||
def __str__(self) -> builtins.str: | ||
... | ||
|
||
|
||
def __repr__(self, /, foo) -> str: | ||
... | ||
|
||
|
||
def __repr__(self, *, foo) -> str: | ||
... | ||
|
||
|
||
class ShouldRemoveSingle: | ||
def __str__(self) -> builtins.str: | ||
... | ||
|
||
|
||
class ShouldRemove: | ||
def __repr__(self) -> str: | ||
... | ||
|
||
def __str__(self) -> builtins.str: | ||
... | ||
|
||
|
||
class NoReturnSpecified: | ||
def __str__(self): | ||
... | ||
|
||
def __repr__(self): | ||
... | ||
|
||
|
||
class NonMatchingArgs: | ||
def __str__(self, *, extra) -> builtins.str: | ||
... | ||
|
||
def __repr__(self, /, extra) -> str: | ||
... | ||
|
||
|
||
class MatchingArgsButAbstract: | ||
@abstractmethod | ||
def __str__(self) -> builtins.str: | ||
... | ||
|
||
@abstractmethod | ||
def __repr__(self) -> str: | ||
... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import builtins | ||
from abc import abstractmethod | ||
|
||
def __repr__(self) -> str: ... | ||
def __str__(self) -> builtins.str: ... | ||
def __repr__(self, /, foo) -> str: ... | ||
def __repr__(self, *, foo) -> str: ... | ||
|
||
class ShouldRemoveSingle: | ||
def __str__(self) -> builtins.str: ... | ||
|
||
class ShouldRemove: | ||
def __repr__(self) -> str: ... | ||
def __str__(self) -> builtins.str: ... | ||
|
||
class NoReturnSpecified: | ||
def __str__(self): ... | ||
def __repr__(self): ... | ||
|
||
class NonMatchingArgs: | ||
def __str__(self, *, extra) -> builtins.str: ... | ||
def __repr__(self, /, extra) -> str: ... | ||
|
||
class MatchingArgsButAbstract: | ||
@abstractmethod | ||
def __str__(self) -> builtins.str: ... | ||
@abstractmethod | ||
def __repr__(self) -> str: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
use rustpython_parser::ast; | ||
use rustpython_parser::ast::{Ranged, Stmt}; | ||
|
||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_semantic::analyze::visibility::is_abstract; | ||
|
||
use crate::autofix::edits::delete_stmt; | ||
use crate::checkers::ast::Checker; | ||
use crate::registry::AsRule; | ||
|
||
/// ## What it does | ||
/// Checks for redundant definitions of `__str__` or `__repr__` in stubs. | ||
/// | ||
/// ## Why is this bad? | ||
/// These definitions are redundant with `object.__str__` or `object.__repr__`. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// class Foo: | ||
/// def __repr__(self) -> str: ... | ||
/// ``` | ||
#[violation] | ||
pub struct StrOrReprDefinedInStub { | ||
name: String, | ||
} | ||
|
||
impl AlwaysAutofixableViolation for StrOrReprDefinedInStub { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
let StrOrReprDefinedInStub { name } = self; | ||
format!("Defining `{name}` in a stub is almost always redundant") | ||
} | ||
|
||
fn autofix_title(&self) -> String { | ||
let StrOrReprDefinedInStub { name } = self; | ||
format!("Remove definition of `{name}`") | ||
} | ||
} | ||
|
||
/// PYI029 | ||
pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { | ||
let Stmt::FunctionDef(ast::StmtFunctionDef { | ||
name, | ||
decorator_list, | ||
returns, | ||
args, | ||
.. | ||
}) = stmt else { | ||
return | ||
}; | ||
|
||
if !matches!(name.as_str(), "__str__" | "__repr__") { | ||
return; | ||
} | ||
|
||
if !checker.semantic_model().scope().kind.is_class() { | ||
return; | ||
} | ||
|
||
// It is a violation only if the method signature matches that of `object.__str__` | ||
// or `object.__repr__` exactly and the method is not decorated as abstract. | ||
if !args.kwonlyargs.is_empty() || (args.args.len() + args.posonlyargs.len()) > 1 { | ||
return; | ||
} | ||
|
||
if is_abstract(checker.semantic_model(), decorator_list) { | ||
return; | ||
} | ||
|
||
let Some(returns) = returns else { | ||
return; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Moving this check to line 54 could speed up performance because it is cheaper than any semantic model check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Done |
||
|
||
if checker | ||
.semantic_model() | ||
.resolve_call_path(returns) | ||
.map_or(true, |call_path| { | ||
!matches!(call_path.as_slice(), ["" | "builtins", "str"]) | ||
}) | ||
{ | ||
return; | ||
} | ||
|
||
let mut diagnostic = Diagnostic::new( | ||
StrOrReprDefinedInStub { | ||
name: name.to_string(), | ||
}, | ||
stmt.range(), | ||
); | ||
|
||
if checker.patch(diagnostic.kind.rule()) { | ||
let mut edit = delete_stmt( | ||
stmt, | ||
checker.semantic_model().stmt_parent(), | ||
checker.locator, | ||
checker.indexer, | ||
checker.stylist, | ||
); | ||
|
||
// If we removed the last statement, replace it with `...` instead of `pass` since we're | ||
// editing a stub. | ||
if edit.content() == Some("pass") { | ||
edit = Edit::range_replacement("...".to_string(), edit.range()); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @charliermarsh is this still necessary after your isolation changes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes although I'm tempted to just let it add |
||
|
||
diagnostic.set_fix( | ||
Fix::automatic(edit).isolate(checker.isolation(checker.semantic_model().stmt_parent())), | ||
); | ||
} | ||
|
||
checker.diagnostics.push(diagnostic); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_pyi/mod.rs | ||
--- | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_pyi/mod.rs | ||
--- | ||
PYI029.pyi:17:5: PYI029 [*] Defining `__str__` in a stub is almost always redundant | ||
| | ||
17 | class ShouldRemoveSingle: | ||
18 | def __str__(self) -> builtins.str: ... | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI029 | ||
19 | | ||
20 | class ShouldRemove: | ||
| | ||
= help: Remove definition of `str` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why the underscores are lost in |
||
|
||
ℹ Fix | ||
14 14 | def __repr__(self, *, foo) -> str: ... | ||
15 15 | | ||
16 16 | class ShouldRemoveSingle: | ||
17 |- def __str__(self) -> builtins.str: ... | ||
17 |+ ... | ||
18 18 | | ||
19 19 | class ShouldRemove: | ||
20 20 | def __repr__(self) -> str: ... | ||
|
||
PYI029.pyi:20:5: PYI029 [*] Defining `__repr__` in a stub is almost always redundant | ||
| | ||
20 | class ShouldRemove: | ||
21 | def __repr__(self) -> str: ... | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI029 | ||
22 | | ||
23 | def __str__(self) -> builtins.str: ... | ||
| | ||
= help: Remove definition of `repr` | ||
|
||
ℹ Fix | ||
17 17 | def __str__(self) -> builtins.str: ... | ||
18 18 | | ||
19 19 | class ShouldRemove: | ||
20 |- def __repr__(self) -> str: ... | ||
21 20 | | ||
22 21 | def __str__(self) -> builtins.str: ... | ||
23 22 | | ||
|
||
PYI029.pyi:22:5: PYI029 [*] Defining `__str__` in a stub is almost always redundant | ||
| | ||
22 | def __repr__(self) -> str: ... | ||
23 | | ||
24 | def __str__(self) -> builtins.str: ... | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI029 | ||
| | ||
= help: Remove definition of `str` | ||
|
||
ℹ Fix | ||
19 19 | class ShouldRemove: | ||
20 20 | def __repr__(self) -> str: ... | ||
21 21 | | ||
22 |- def __str__(self) -> builtins.str: ... | ||
23 22 | | ||
24 23 | | ||
25 24 | class NoReturnSpecified: | ||
|
||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this rule also apply for
async
function definitions?If so, then the following could be useful (I thought we already had this, but I wasn't able to find it with a quick search).
In
rustpython-ast
define a newAnyFunctionDef
node that is a union over sync and async function definitionsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I can take a look at this separate from this PR.)