Skip to content

Commit

Permalink
[pylint] Implement bad-dunder-name (W3201) (#6486)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
LaBatata101 committed Aug 11, 2023
1 parent 9ff80a8 commit eb68add
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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
...
3 changes: 3 additions & 0 deletions crates/ruff/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/rules/pylint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
192 changes: 192 additions & 0 deletions crates/ruff/src/rules/pylint/rules/bad_dunder_method_name.rs
Original file line number Diff line number Diff line change
@@ -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__"
)
}
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/pylint/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit eb68add

Please sign in to comment.