Skip to content

Commit 229a50a

Browse files
[pylint] Implement singledispatchmethod-function (PLE5120) (#10428)
## Summary Implement `singledispatchmethod-function` from pylint, part of #970. This is essentially a copy paste of #8934 for `@singledispatchmethod` decorator. ## Test Plan Text fixture added.
1 parent 8619986 commit 229a50a

File tree

8 files changed

+207
-0
lines changed

8 files changed

+207
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from functools import singledispatchmethod
2+
3+
4+
@singledispatchmethod # [singledispatchmethod-function]
5+
def convert_position(position):
6+
pass
7+
8+
9+
class Board:
10+
11+
@singledispatchmethod # Ok
12+
@classmethod
13+
def convert_position(cls, position):
14+
pass
15+
16+
@singledispatchmethod # Ok
17+
def move(self, position):
18+
pass
19+
20+
@singledispatchmethod # [singledispatchmethod-function]
21+
@staticmethod
22+
def do(position):
23+
pass

crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
4343
Rule::UnusedStaticMethodArgument,
4444
Rule::UnusedVariable,
4545
Rule::SingledispatchMethod,
46+
Rule::SingledispatchmethodFunction,
4647
]) {
4748
return;
4849
}
@@ -419,6 +420,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
419420
pylint::rules::singledispatch_method(checker, scope, &mut diagnostics);
420421
}
421422

423+
if checker.enabled(Rule::SingledispatchmethodFunction) {
424+
pylint::rules::singledispatchmethod_function(checker, scope, &mut diagnostics);
425+
}
426+
422427
if checker.any_enabled(&[
423428
Rule::InvalidFirstArgumentNameForClassMethod,
424429
Rule::InvalidFirstArgumentNameForMethod,

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
256256
(Pylint, "E1310") => (RuleGroup::Stable, rules::pylint::rules::BadStrStripCall),
257257
(Pylint, "E1507") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarValue),
258258
(Pylint, "E1519") => (RuleGroup::Preview, rules::pylint::rules::SingledispatchMethod),
259+
(Pylint, "E1520") => (RuleGroup::Preview, rules::pylint::rules::SingledispatchmethodFunction),
259260
(Pylint, "E1700") => (RuleGroup::Stable, rules::pylint::rules::YieldFromInAsyncFunction),
260261
(Pylint, "E2502") => (RuleGroup::Stable, rules::pylint::rules::BidirectionalUnicode),
261262
(Pylint, "E2510") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterBackspace),

crates/ruff_linter/src/rules/pylint/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ mod tests {
2121
use crate::test::test_path;
2222

2323
#[test_case(Rule::SingledispatchMethod, Path::new("singledispatch_method.py"))]
24+
#[test_case(
25+
Rule::SingledispatchmethodFunction,
26+
Path::new("singledispatchmethod_function.py")
27+
)]
2428
#[test_case(Rule::AssertOnStringLiteral, Path::new("assert_on_string_literal.py"))]
2529
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async.py"))]
2630
#[test_case(Rule::BadOpenMode, Path::new("bad_open_mode.py"))]

crates/ruff_linter/src/rules/pylint/rules/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub(crate) use return_in_init::*;
5757
pub(crate) use self_assigning_variable::*;
5858
pub(crate) use single_string_slots::*;
5959
pub(crate) use singledispatch_method::*;
60+
pub(crate) use singledispatchmethod_function::*;
6061
pub(crate) use subprocess_popen_preexec_fn::*;
6162
pub(crate) use subprocess_run_without_check::*;
6263
pub(crate) use super_without_brackets::*;
@@ -147,6 +148,7 @@ mod return_in_init;
147148
mod self_assigning_variable;
148149
mod single_string_slots;
149150
mod singledispatch_method;
151+
mod singledispatchmethod_function;
150152
mod subprocess_popen_preexec_fn;
151153
mod subprocess_run_without_check;
152154
mod super_without_brackets;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast as ast;
4+
use ruff_python_semantic::analyze::function_type;
5+
use ruff_python_semantic::Scope;
6+
use ruff_text_size::Ranged;
7+
8+
use crate::checkers::ast::Checker;
9+
use crate::importer::ImportRequest;
10+
11+
/// ## What it does
12+
/// Checks for `@singledispatchmethod` decorators on functions or static
13+
/// methods.
14+
///
15+
/// ## Why is this bad?
16+
/// The `@singledispatchmethod` decorator is intended for use with class and
17+
/// instance methods, not functions.
18+
///
19+
/// Instead, use the `@singledispatch` decorator.
20+
///
21+
/// ## Example
22+
/// ```python
23+
/// from functools import singledispatchmethod
24+
///
25+
///
26+
/// @singledispatchmethod
27+
/// def func(arg):
28+
/// ...
29+
/// ```
30+
///
31+
/// Use instead:
32+
/// ```python
33+
/// from functools import singledispatchmethod
34+
///
35+
///
36+
/// @singledispatch
37+
/// def func(arg):
38+
/// ...
39+
/// ```
40+
///
41+
/// ## Fix safety
42+
/// This rule's fix is marked as unsafe, as migrating from `@singledispatchmethod` to
43+
/// `@singledispatch` may change the behavior of the code.
44+
#[violation]
45+
pub struct SingledispatchmethodFunction;
46+
47+
impl Violation for SingledispatchmethodFunction {
48+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
49+
50+
#[derive_message_formats]
51+
fn message(&self) -> String {
52+
format!("`@singledispatchmethod` decorator should not be used on non-method functions")
53+
}
54+
55+
fn fix_title(&self) -> Option<String> {
56+
Some("Replace with `@singledispatch`".to_string())
57+
}
58+
}
59+
60+
/// E1520
61+
pub(crate) fn singledispatchmethod_function(
62+
checker: &Checker,
63+
scope: &Scope,
64+
diagnostics: &mut Vec<Diagnostic>,
65+
) {
66+
let Some(func) = scope.kind.as_function() else {
67+
return;
68+
};
69+
70+
let ast::StmtFunctionDef {
71+
name,
72+
decorator_list,
73+
..
74+
} = func;
75+
76+
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
77+
return;
78+
};
79+
80+
let type_ = function_type::classify(
81+
name,
82+
decorator_list,
83+
parent,
84+
checker.semantic(),
85+
&checker.settings.pep8_naming.classmethod_decorators,
86+
&checker.settings.pep8_naming.staticmethod_decorators,
87+
);
88+
if !matches!(
89+
type_,
90+
function_type::FunctionType::Function | function_type::FunctionType::StaticMethod
91+
) {
92+
return;
93+
}
94+
95+
for decorator in decorator_list {
96+
if checker
97+
.semantic()
98+
.resolve_qualified_name(&decorator.expression)
99+
.is_some_and(|qualified_name| {
100+
matches!(
101+
qualified_name.segments(),
102+
["functools", "singledispatchmethod"]
103+
)
104+
})
105+
{
106+
let mut diagnostic = Diagnostic::new(SingledispatchmethodFunction, decorator.range());
107+
diagnostic.try_set_fix(|| {
108+
let (import_edit, binding) = checker.importer().get_or_import_symbol(
109+
&ImportRequest::import("functools", "singledispatch"),
110+
decorator.start(),
111+
checker.semantic(),
112+
)?;
113+
Ok(Fix::unsafe_edits(
114+
Edit::range_replacement(binding, decorator.expression.range()),
115+
[import_edit],
116+
))
117+
});
118+
diagnostics.push(diagnostic);
119+
}
120+
}
121+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pylint/mod.rs
3+
---
4+
singledispatchmethod_function.py:4:1: PLE1520 [*] `@singledispatchmethod` decorator should not be used on non-method functions
5+
|
6+
4 | @singledispatchmethod # [singledispatchmethod-function]
7+
| ^^^^^^^^^^^^^^^^^^^^^ PLE1520
8+
5 | def convert_position(position):
9+
6 | pass
10+
|
11+
= help: Replace with `@singledispatch`
12+
13+
ℹ Unsafe fix
14+
1 |-from functools import singledispatchmethod
15+
1 |+from functools import singledispatchmethod, singledispatch
16+
2 2 |
17+
3 3 |
18+
4 |-@singledispatchmethod # [singledispatchmethod-function]
19+
4 |+@singledispatch # [singledispatchmethod-function]
20+
5 5 | def convert_position(position):
21+
6 6 | pass
22+
7 7 |
23+
24+
singledispatchmethod_function.py:20:5: PLE1520 [*] `@singledispatchmethod` decorator should not be used on non-method functions
25+
|
26+
18 | pass
27+
19 |
28+
20 | @singledispatchmethod # [singledispatchmethod-function]
29+
| ^^^^^^^^^^^^^^^^^^^^^ PLE1520
30+
21 | @staticmethod
31+
22 | def do(position):
32+
|
33+
= help: Replace with `@singledispatch`
34+
35+
ℹ Unsafe fix
36+
1 |-from functools import singledispatchmethod
37+
1 |+from functools import singledispatchmethod, singledispatch
38+
2 2 |
39+
3 3 |
40+
4 4 | @singledispatchmethod # [singledispatchmethod-function]
41+
--------------------------------------------------------------------------------
42+
17 17 | def move(self, position):
43+
18 18 | pass
44+
19 19 |
45+
20 |- @singledispatchmethod # [singledispatchmethod-function]
46+
20 |+ @singledispatch # [singledispatchmethod-function]
47+
21 21 | @staticmethod
48+
22 22 | def do(position):
49+
23 23 | pass

ruff.schema.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)