Skip to content
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

[pylint] Implement bad-dunder-name (W3201) #6486

Merged
merged 6 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Loading