Skip to content

Commit 9ab497d

Browse files
add singledispatchmethod-function
1 parent a8e50a7 commit 9ab497d

File tree

8 files changed

+200
-0
lines changed

8 files changed

+200
-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

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

2323
#[test_case(Rule::SingledispatchMethod, Path::new("singledispatch_method.py"))]
24+
#[test_case(Rule::SingledispatchmethodFunction, Path::new("singledispatchmethod_function.py"))]
2425
#[test_case(Rule::AssertOnStringLiteral, Path::new("assert_on_string_literal.py"))]
2526
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async.py"))]
2627
#[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,116 @@
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!(qualified_name.segments(), ["functools", "singledispatchmethod"])
99+
})
100+
{
101+
let mut diagnostic = Diagnostic::new(SingledispatchmethodFunction, decorator.range());
102+
diagnostic.try_set_fix(|| {
103+
let (import_edit, binding) = checker.importer().get_or_import_symbol(
104+
&ImportRequest::import("functools", "singledispatch"),
105+
decorator.start(),
106+
checker.semantic(),
107+
)?;
108+
Ok(Fix::unsafe_edits(
109+
Edit::range_replacement(binding, decorator.expression.range()),
110+
[import_edit],
111+
))
112+
});
113+
diagnostics.push(diagnostic);
114+
}
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pylint/mod.rs
3+
assertion_line: 200
4+
---
5+
singledispatchmethod_function.py:4:1: PLE1520 [*] `@singledispatchmethod` decorator should not be used on functions
6+
|
7+
4 | @singledispatchmethod # [singledispatchmethod-function]
8+
| ^^^^^^^^^^^^^^^^^^^^^ PLE1520
9+
5 | def convert_position(position):
10+
6 | pass
11+
|
12+
= help: Replace with `@singledispatch`
13+
14+
ℹ Unsafe fix
15+
1 |-from functools import singledispatchmethod
16+
1 |+from functools import singledispatchmethod, singledispatch
17+
2 2 |
18+
3 3 |
19+
4 |-@singledispatchmethod # [singledispatchmethod-function]
20+
4 |+@singledispatch # [singledispatchmethod-function]
21+
5 5 | def convert_position(position):
22+
6 6 | pass
23+
7 7 |
24+
25+
singledispatchmethod_function.py:20:5: PLE1520 [*] `@singledispatchmethod` decorator should not be used on functions
26+
|
27+
18 | pass
28+
19 |
29+
20 | @singledispatchmethod # [singledispatchmethod-function]
30+
| ^^^^^^^^^^^^^^^^^^^^^ PLE1520
31+
21 | @staticmethod
32+
22 | def do(position):
33+
|
34+
= help: Replace with `@singledispatch`
35+
36+
ℹ Unsafe fix
37+
1 |-from functools import singledispatchmethod
38+
1 |+from functools import singledispatchmethod, singledispatch
39+
2 2 |
40+
3 3 |
41+
4 4 | @singledispatchmethod # [singledispatchmethod-function]
42+
--------------------------------------------------------------------------------
43+
17 17 | def move(self, position):
44+
18 18 | pass
45+
19 19 |
46+
20 |- @singledispatchmethod # [singledispatchmethod-function]
47+
20 |+ @singledispatch # [singledispatchmethod-function]
48+
21 21 | @staticmethod
49+
22 22 | def do(position):
50+
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)