Skip to content

Commit 5d09a08

Browse files
add singledispatchmethod-function
1 parent a8e50a7 commit 5d09a08

File tree

8 files changed

+205
-0
lines changed

8 files changed

+205
-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
}
@@ -403,6 +404,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
403404
pylint::rules::singledispatch_method(checker, scope, &mut diagnostics);
404405
}
405406

407+
if checker.enabled(Rule::SingledispatchmethodFunction) {
408+
pylint::rules::singledispatchmethod_function(checker, scope, &mut diagnostics);
409+
}
410+
406411
if checker.any_enabled(&[
407412
Rule::InvalidFirstArgumentNameForClassMethod,
408413
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
@@ -56,6 +56,7 @@ pub(crate) use return_in_init::*;
5656
pub(crate) use self_assigning_variable::*;
5757
pub(crate) use single_string_slots::*;
5858
pub(crate) use singledispatch_method::*;
59+
pub(crate) use singledispatchmethod_function::*;
5960
pub(crate) use subprocess_popen_preexec_fn::*;
6061
pub(crate) use subprocess_run_without_check::*;
6162
pub(crate) use super_without_brackets::*;
@@ -145,6 +146,7 @@ mod return_in_init;
145146
mod self_assigning_variable;
146147
mod single_string_slots;
147148
mod singledispatch_method;
149+
mod singledispatchmethod_function;
148150
mod subprocess_popen_preexec_fn;
149151
mod subprocess_run_without_check;
150152
mod super_without_brackets;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 methods.
13+
///
14+
/// ## Why is this bad?
15+
/// The `@singledispatchmethod` decorator is intended for use with class and instance methods, not functions.
16+
///
17+
/// Instead, use the `@singledispatch` decorator.
18+
///
19+
/// ## Example
20+
/// ```python
21+
/// from functools import singledispatchmethod
22+
///
23+
///
24+
/// @singledispatchmethod
25+
/// def foo(arg):
26+
/// ...
27+
/// ```
28+
///
29+
/// Use instead:
30+
/// ```python
31+
/// from functools import singledispatchmethod
32+
///
33+
///
34+
/// @singledispatch
35+
/// def foo(arg):
36+
/// ...
37+
/// ```
38+
///
39+
/// ## Fix safety
40+
/// This rule's fix is marked as unsafe, as migrating from `@singledispatchmethod` to
41+
/// `@singledispatch` may change the behavior of the code.
42+
#[violation]
43+
pub struct SingledispatchmethodFunction;
44+
45+
impl Violation for SingledispatchmethodFunction {
46+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
47+
48+
#[derive_message_formats]
49+
fn message(&self) -> String {
50+
format!("`@singledispatchmethod` decorator should not be used on functions")
51+
}
52+
53+
fn fix_title(&self) -> Option<String> {
54+
Some("Replace with `@singledispatch`".to_string())
55+
}
56+
}
57+
58+
/// E1520
59+
pub(crate) fn singledispatchmethod_function(
60+
checker: &Checker,
61+
scope: &Scope,
62+
diagnostics: &mut Vec<Diagnostic>,
63+
) {
64+
let Some(func) = scope.kind.as_function() else {
65+
return;
66+
};
67+
68+
let ast::StmtFunctionDef {
69+
name,
70+
decorator_list,
71+
..
72+
} = func;
73+
74+
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
75+
return;
76+
};
77+
78+
let type_ = function_type::classify(
79+
name,
80+
decorator_list,
81+
parent,
82+
checker.semantic(),
83+
&checker.settings.pep8_naming.classmethod_decorators,
84+
&checker.settings.pep8_naming.staticmethod_decorators,
85+
);
86+
if !matches!(
87+
type_,
88+
function_type::FunctionType::Function | function_type::FunctionType::StaticMethod
89+
) {
90+
return;
91+
}
92+
93+
for decorator in decorator_list {
94+
if checker
95+
.semantic()
96+
.resolve_qualified_name(&decorator.expression)
97+
.is_some_and(|qualified_name| {
98+
matches!(
99+
qualified_name.segments(),
100+
["functools", "singledispatchmethod"]
101+
)
102+
})
103+
{
104+
let mut diagnostic = Diagnostic::new(SingledispatchmethodFunction, decorator.range());
105+
diagnostic.try_set_fix(|| {
106+
let (import_edit, binding) = checker.importer().get_or_import_symbol(
107+
&ImportRequest::import("functools", "singledispatch"),
108+
decorator.start(),
109+
checker.semantic(),
110+
)?;
111+
Ok(Fix::unsafe_edits(
112+
Edit::range_replacement(binding, decorator.expression.range()),
113+
[import_edit],
114+
))
115+
});
116+
diagnostics.push(diagnostic);
117+
}
118+
}
119+
}
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 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 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)