Skip to content

Commit 71fabe2

Browse files
committed
Make SLF diagnostics robust to non-self-named variables
1 parent d11fd4b commit 71fabe2

File tree

6 files changed

+213
-9
lines changed

6 files changed

+213
-9
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# https://github.com/astral-sh/ruff/issues/24275
2+
3+
class C:
4+
def false_positive(this):
5+
this._x = 0 # fine
6+
7+
@classmethod
8+
def false_positive_2(that):
9+
return that._x # fine
10+
11+
def false_negative_1(this, self):
12+
self._x = 1 # error
13+
14+
@classmethod
15+
def false_positive_3(self):
16+
return self._x # fine
17+
18+
@staticmethod
19+
def false_negative_3(self):
20+
return self._x # error
21+
22+
23+
def false_negative_4(self):
24+
return self._x # error
25+
26+
27+
def false_negative_5():
28+
self = C()
29+
return self._x # error
30+
31+
32+
self = C()
33+
34+
35+
def false_negative_6():
36+
return self._x # error
37+
38+
39+
def false_negative_7(cls):
40+
return cls._x # error
41+
42+
43+
def false_negative_8():
44+
mcs = C
45+
return mcs._x # error
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
def custom_classmethod(func):
2+
return classmethod(func)
3+
4+
5+
def custom_staticmethod(func):
6+
return staticmethod(func)
7+
8+
9+
class C:
10+
def ok(this):
11+
return this._x # fine
12+
13+
@custom_classmethod
14+
def ok_classmethod(this):
15+
return this._x # fine
16+
17+
@custom_staticmethod
18+
def bad_staticmethod(self):
19+
return self._x # error

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod tests {
1616

1717
#[test_case(Rule::PrivateMemberAccess, Path::new("SLF001.py"))]
1818
#[test_case(Rule::PrivateMemberAccess, Path::new("SLF001_1.py"))]
19+
#[test_case(Rule::PrivateMemberAccess, Path::new("SLF001_2.py"))]
1920
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
2021
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
2122
let diagnostics = test_path(
@@ -40,4 +41,21 @@ mod tests {
4041
assert_diagnostics!(diagnostics);
4142
Ok(())
4243
}
44+
45+
#[test]
46+
fn custom_method_decorators() -> Result<()> {
47+
let diagnostics = test_path(
48+
Path::new("flake8_self/SLF001_custom_decorators.py"),
49+
&settings::LinterSettings {
50+
pep8_naming: crate::rules::pep8_naming::settings::Settings {
51+
classmethod_decorators: vec!["custom_classmethod".to_string()],
52+
staticmethod_decorators: vec!["custom_staticmethod".to_string()],
53+
..Default::default()
54+
},
55+
..settings::LinterSettings::for_rule(Rule::PrivateMemberAccess)
56+
},
57+
)?;
58+
assert_diagnostics!(diagnostics);
59+
Ok(())
60+
}
4361
}

crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
22
use ruff_python_ast::helpers::{is_dunder, is_sunder};
33
use ruff_python_ast::name::UnqualifiedName;
44
use ruff_python_ast::{self as ast, Expr};
5+
use ruff_python_semantic::analyze::function_type;
56
use ruff_python_semantic::analyze::typing;
67
use ruff_python_semantic::analyze::typing::TypeChecker;
78
use ruff_python_semantic::{BindingKind, ScopeKind, SemanticModel};
@@ -117,13 +118,6 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) {
117118
}
118119
}
119120

120-
if let Some(name) = UnqualifiedName::from_expr(value) {
121-
// Ignore `self` and `cls` accesses.
122-
if matches!(name.segments(), ["self" | "cls" | "mcs"]) {
123-
return;
124-
}
125-
}
126-
127121
if let Expr::Name(name) = value.as_ref() {
128122
// Ignore accesses on class members from _within_ the class.
129123
if semantic
@@ -140,7 +134,7 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) {
140134
return;
141135
}
142136

143-
if is_same_class_instance(name, semantic) {
137+
if is_same_class_instance(checker, name) {
144138
return;
145139
}
146140
}
@@ -177,7 +171,12 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) {
177171
///
178172
/// This function is intentionally naive and does not handle more complex cases.
179173
/// It is expected to be expanded overtime, possibly when type-aware APIs are available.
180-
fn is_same_class_instance(name: &ast::ExprName, semantic: &SemanticModel) -> bool {
174+
fn is_same_class_instance(checker: &Checker, name: &ast::ExprName) -> bool {
175+
if is_method_receiver(checker, name) {
176+
return true;
177+
}
178+
179+
let semantic = checker.semantic();
181180
let Some(binding_id) = semantic.resolve_name(name) else {
182181
return false;
183182
};
@@ -186,6 +185,55 @@ fn is_same_class_instance(name: &ast::ExprName, semantic: &SemanticModel) -> boo
186185
typing::check_type::<SameClassInstanceChecker>(binding, semantic)
187186
}
188187

188+
fn is_method_receiver(checker: &Checker, name: &ast::ExprName) -> bool {
189+
let semantic = checker.semantic();
190+
191+
let Some(binding_id) = semantic.resolve_name(name) else {
192+
return false;
193+
};
194+
let binding = semantic.binding(binding_id);
195+
196+
if !matches!(binding.kind, BindingKind::Argument) {
197+
return false;
198+
}
199+
200+
let Some(ast::Stmt::FunctionDef(function)) = binding.statement(semantic) else {
201+
return false;
202+
};
203+
204+
let Some(first_parameter) = function
205+
.parameters
206+
.posonlyargs
207+
.first()
208+
.or_else(|| function.parameters.args.first())
209+
else {
210+
return false;
211+
};
212+
213+
if binding.range != first_parameter.parameter.name.range() {
214+
return false;
215+
}
216+
217+
let scope = &semantic.scopes[binding.scope];
218+
let Some(parent_scope) = semantic.first_non_type_parent_scope(scope) else {
219+
return false;
220+
};
221+
222+
matches!(
223+
function_type::classify(
224+
&function.name,
225+
&function.decorator_list,
226+
parent_scope,
227+
semantic,
228+
&checker.settings().pep8_naming.classmethod_decorators,
229+
&checker.settings().pep8_naming.staticmethod_decorators,
230+
),
231+
function_type::FunctionType::Method
232+
| function_type::FunctionType::ClassMethod
233+
| function_type::FunctionType::NewMethod
234+
)
235+
}
236+
189237
struct SameClassInstanceChecker;
190238

191239
impl SameClassInstanceChecker {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: crates/ruff_linter/src/rules/flake8_self/mod.rs
3+
---
4+
SLF001 Private member accessed: `_x`
5+
--> SLF001_custom_decorators.py:19:16
6+
|
7+
17 | @custom_staticmethod
8+
18 | def bad_staticmethod(self):
9+
19 | return self._x # error
10+
| ^^^^^^^
11+
|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
source: crates/ruff_linter/src/rules/flake8_self/mod.rs
3+
---
4+
SLF001 Private member accessed: `_x`
5+
--> SLF001_2.py:12:9
6+
|
7+
11 | def false_negative_1(this, self):
8+
12 | self._x = 1 # error
9+
| ^^^^^^^
10+
13 |
11+
14 | @classmethod
12+
|
13+
14+
SLF001 Private member accessed: `_x`
15+
--> SLF001_2.py:20:16
16+
|
17+
18 | @staticmethod
18+
19 | def false_negative_3(self):
19+
20 | return self._x # error
20+
| ^^^^^^^
21+
|
22+
23+
SLF001 Private member accessed: `_x`
24+
--> SLF001_2.py:24:12
25+
|
26+
23 | def false_negative_4(self):
27+
24 | return self._x # error
28+
| ^^^^^^^
29+
|
30+
31+
SLF001 Private member accessed: `_x`
32+
--> SLF001_2.py:29:12
33+
|
34+
27 | def false_negative_5():
35+
28 | self = C()
36+
29 | return self._x # error
37+
| ^^^^^^^
38+
|
39+
40+
SLF001 Private member accessed: `_x`
41+
--> SLF001_2.py:36:12
42+
|
43+
35 | def false_negative_6():
44+
36 | return self._x # error
45+
| ^^^^^^^
46+
|
47+
48+
SLF001 Private member accessed: `_x`
49+
--> SLF001_2.py:40:12
50+
|
51+
39 | def false_negative_7(cls):
52+
40 | return cls._x # error
53+
| ^^^^^^
54+
|
55+
56+
SLF001 Private member accessed: `_x`
57+
--> SLF001_2.py:45:12
58+
|
59+
43 | def false_negative_8():
60+
44 | mcs = C
61+
45 | return mcs._x # error
62+
| ^^^^^^
63+
|

0 commit comments

Comments
 (0)