[pylint] Implement bad-dunder-name (W3201) (#6486)
## Summary Checks for any misspelled dunder name method and for any method defined with `__...__` that's not one of the pre-defined methods. The pre-defined methods encompass all of Python's standard dunder methods. ref: #970 ## Test Plan Snapshots and manual runs of pylint.
This commit is contained in:
committed by
GitHub
parent
9ff80a82b4
commit
eb68addf97
46
crates/ruff/resources/test/fixtures/pylint/bad_dunder_method_name.py
vendored
Normal file
46
crates/ruff/resources/test/fixtures/pylint/bad_dunder_method_name.py
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
class Apples:
|
||||
def _init_(self): # [bad-dunder-name]
|
||||
pass
|
||||
|
||||
def __hello__(self): # [bad-dunder-name]
|
||||
print("hello")
|
||||
|
||||
def __init_(self): # [bad-dunder-name]
|
||||
# author likely unintentionally misspelled the correct init dunder.
|
||||
pass
|
||||
|
||||
def _init_(self): # [bad-dunder-name]
|
||||
# author likely unintentionally misspelled the correct init dunder.
|
||||
pass
|
||||
|
||||
def ___neg__(self): # [bad-dunder-name]
|
||||
# author likely accidentally added an additional `_`
|
||||
pass
|
||||
|
||||
def __inv__(self): # [bad-dunder-name]
|
||||
# author likely meant to call the invert dunder method
|
||||
pass
|
||||
|
||||
def hello(self):
|
||||
print("hello")
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def init(self):
|
||||
# valid name even though someone could accidentally mean __init__
|
||||
pass
|
||||
|
||||
def _protected_method(self):
|
||||
print("Protected")
|
||||
|
||||
def __private_method(self):
|
||||
print("Private")
|
||||
|
||||
@property
|
||||
def __doc__(self):
|
||||
return "Docstring"
|
||||
|
||||
|
||||
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
|
||||
...
|
||||
@@ -509,6 +509,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SingleStringSlots) {
|
||||
pylint::rules::single_string_slots(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::BadDunderMethodName) {
|
||||
pylint::rules::bad_dunder_method_name(checker, body);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if checker.enabled(Rule::MultipleImportsOnOneLine) {
|
||||
|
||||
@@ -229,6 +229,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
|
||||
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
|
||||
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
|
||||
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
|
||||
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
|
||||
|
||||
// flake8-async
|
||||
|
||||
@@ -130,6 +130,7 @@ mod tests {
|
||||
Rule::SubprocessRunWithoutCheck,
|
||||
Path::new("subprocess_run_without_check.py")
|
||||
)]
|
||||
#[test_case(Rule::BadDunderMethodName, Path::new("bad_dunder_method_name.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
192
crates/ruff/src/rules/pylint/rules/bad_dunder_method_name.rs
Normal file
192
crates/ruff/src/rules/pylint/rules/bad_dunder_method_name.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::Stmt;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for any misspelled dunder name method and for any method
|
||||
/// defined with `__...__` that's not one of the pre-defined methods.
|
||||
///
|
||||
/// The pre-defined methods encompass all of Python's standard dunder
|
||||
/// methods.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Misspelled dunder name methods may cause your code to not function
|
||||
/// as expected.
|
||||
///
|
||||
/// Since dunder methods are associated with customizing the behavior
|
||||
/// of a class in Python, introducing a dunder method such as `__foo__`
|
||||
/// that diverges from standard Python dunder methods could potentially
|
||||
/// confuse someone reading the code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __init_(self):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __init__(self):
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BadDunderMethodName {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for BadDunderMethodName {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BadDunderMethodName { name } = self;
|
||||
format!("Bad or misspelled dunder method name `{name}`. (bad-dunder-name)")
|
||||
}
|
||||
}
|
||||
|
||||
/// PLW3201
|
||||
pub(crate) fn bad_dunder_method_name(checker: &mut Checker, class_body: &[Stmt]) {
|
||||
for method in class_body
|
||||
.iter()
|
||||
.filter_map(ruff_python_ast::Stmt::as_function_def_stmt)
|
||||
.filter(|method| {
|
||||
if is_known_dunder_method(&method.name) {
|
||||
return false;
|
||||
}
|
||||
method.name.starts_with('_') && method.name.ends_with('_')
|
||||
})
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadDunderMethodName {
|
||||
name: method.name.to_string(),
|
||||
},
|
||||
method.identifier(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a method is a known dunder method.
|
||||
fn is_known_dunder_method(method: &str) -> bool {
|
||||
matches!(
|
||||
method,
|
||||
"__abs__"
|
||||
| "__add__"
|
||||
| "__aenter__"
|
||||
| "__aexit__"
|
||||
| "__aiter__"
|
||||
| "__and__"
|
||||
| "__anext__"
|
||||
| "__await__"
|
||||
| "__bool__"
|
||||
| "__bytes__"
|
||||
| "__call__"
|
||||
| "__ceil__"
|
||||
| "__class__"
|
||||
| "__class_getitem__"
|
||||
| "__complex__"
|
||||
| "__contains__"
|
||||
| "__copy__"
|
||||
| "__deepcopy__"
|
||||
| "__del__"
|
||||
| "__delattr__"
|
||||
| "__delete__"
|
||||
| "__delitem__"
|
||||
| "__dict__"
|
||||
| "__dir__"
|
||||
| "__divmod__"
|
||||
| "__doc__"
|
||||
| "__enter__"
|
||||
| "__eq__"
|
||||
| "__exit__"
|
||||
| "__float__"
|
||||
| "__floor__"
|
||||
| "__floordiv__"
|
||||
| "__format__"
|
||||
| "__fspath__"
|
||||
| "__ge__"
|
||||
| "__get__"
|
||||
| "__getattr__"
|
||||
| "__getattribute__"
|
||||
| "__getitem__"
|
||||
| "__getnewargs__"
|
||||
| "__getnewargs_ex__"
|
||||
| "__getstate__"
|
||||
| "__gt__"
|
||||
| "__hash__"
|
||||
| "__iadd__"
|
||||
| "__iand__"
|
||||
| "__ifloordiv__"
|
||||
| "__ilshift__"
|
||||
| "__imatmul__"
|
||||
| "__imod__"
|
||||
| "__imul__"
|
||||
| "__init__"
|
||||
| "__init_subclass__"
|
||||
| "__instancecheck__"
|
||||
| "__int__"
|
||||
| "__invert__"
|
||||
| "__ior__"
|
||||
| "__ipow__"
|
||||
| "__irshift__"
|
||||
| "__isub__"
|
||||
| "__iter__"
|
||||
| "__itruediv__"
|
||||
| "__ixor__"
|
||||
| "__le__"
|
||||
| "__len__"
|
||||
| "__length_hint__"
|
||||
| "__lshift__"
|
||||
| "__lt__"
|
||||
| "__matmul__"
|
||||
| "__missing__"
|
||||
| "__mod__"
|
||||
| "__module__"
|
||||
| "__mul__"
|
||||
| "__ne__"
|
||||
| "__neg__"
|
||||
| "__new__"
|
||||
| "__next__"
|
||||
| "__or__"
|
||||
| "__pos__"
|
||||
| "__post_init__"
|
||||
| "__pow__"
|
||||
| "__radd__"
|
||||
| "__rand__"
|
||||
| "__rdivmod__"
|
||||
| "__reduce__"
|
||||
| "__reduce_ex__"
|
||||
| "__repr__"
|
||||
| "__reversed__"
|
||||
| "__rfloordiv__"
|
||||
| "__rlshift__"
|
||||
| "__rmatmul__"
|
||||
| "__rmod__"
|
||||
| "__rmul__"
|
||||
| "__ror__"
|
||||
| "__round__"
|
||||
| "__rpow__"
|
||||
| "__rrshift__"
|
||||
| "__rshift__"
|
||||
| "__rsub__"
|
||||
| "__rtruediv__"
|
||||
| "__rxor__"
|
||||
| "__set__"
|
||||
| "__set_name__"
|
||||
| "__setattr__"
|
||||
| "__setitem__"
|
||||
| "__setstate__"
|
||||
| "__sizeof__"
|
||||
| "__str__"
|
||||
| "__sub__"
|
||||
| "__subclasscheck__"
|
||||
| "__subclasses__"
|
||||
| "__subclasshook__"
|
||||
| "__truediv__"
|
||||
| "__trunc__"
|
||||
| "__weakref__"
|
||||
| "__xor__"
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) use assert_on_string_literal::*;
|
||||
pub(crate) use await_outside_async::*;
|
||||
pub(crate) use bad_dunder_method_name::*;
|
||||
pub(crate) use bad_str_strip_call::*;
|
||||
pub(crate) use bad_string_format_character::BadStringFormatCharacter;
|
||||
pub(crate) use bad_string_format_type::*;
|
||||
@@ -56,6 +57,7 @@ pub(crate) use yield_in_init::*;
|
||||
|
||||
mod assert_on_string_literal;
|
||||
mod await_outside_async;
|
||||
mod bad_dunder_method_name;
|
||||
mod bad_str_strip_call;
|
||||
pub(crate) mod bad_string_format_character;
|
||||
mod bad_string_format_type;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
bad_dunder_method_name.py:2:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
|
||||
|
|
||||
1 | class Apples:
|
||||
2 | def _init_(self): # [bad-dunder-name]
|
||||
| ^^^^^^ PLW3201
|
||||
3 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `__hello__`. (bad-dunder-name)
|
||||
|
|
||||
3 | pass
|
||||
4 |
|
||||
5 | def __hello__(self): # [bad-dunder-name]
|
||||
| ^^^^^^^^^ PLW3201
|
||||
6 | print("hello")
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__init_`. (bad-dunder-name)
|
||||
|
|
||||
6 | print("hello")
|
||||
7 |
|
||||
8 | def __init_(self): # [bad-dunder-name]
|
||||
| ^^^^^^^ PLW3201
|
||||
9 | # author likely unintentionally misspelled the correct init dunder.
|
||||
10 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:12:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
|
||||
|
|
||||
10 | pass
|
||||
11 |
|
||||
12 | def _init_(self): # [bad-dunder-name]
|
||||
| ^^^^^^ PLW3201
|
||||
13 | # author likely unintentionally misspelled the correct init dunder.
|
||||
14 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:16:9: PLW3201 Bad or misspelled dunder method name `___neg__`. (bad-dunder-name)
|
||||
|
|
||||
14 | pass
|
||||
15 |
|
||||
16 | def ___neg__(self): # [bad-dunder-name]
|
||||
| ^^^^^^^^ PLW3201
|
||||
17 | # author likely accidentally added an additional `_`
|
||||
18 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:20:9: PLW3201 Bad or misspelled dunder method name `__inv__`. (bad-dunder-name)
|
||||
|
|
||||
18 | pass
|
||||
19 |
|
||||
20 | def __inv__(self): # [bad-dunder-name]
|
||||
| ^^^^^^^ PLW3201
|
||||
21 | # author likely meant to call the invert dunder method
|
||||
22 | pass
|
||||
|
|
||||
|
||||
|
||||
1
ruff.schema.json
generated
1
ruff.schema.json
generated
@@ -2291,6 +2291,7 @@
|
||||
"PLW290",
|
||||
"PLW2901",
|
||||
"PLW3",
|
||||
"PLW3201",
|
||||
"PLW33",
|
||||
"PLW330",
|
||||
"PLW3301",
|
||||
|
||||
Reference in New Issue
Block a user