[flake8-bultins] Detect class-scope builtin shadowing in decorators, default args, and attribute initializers (A003) (#20178)

## Summary
Fix #20171

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Dan Parizher 2025-09-22 18:12:45 -04:00 committed by GitHub
parent 32d00cd569
commit 094bf70a60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 112 additions and 6 deletions

View file

@ -19,3 +19,18 @@ class MyClass:
def attribute_usage(self) -> id:
pass
class C:
@staticmethod
def property(f):
return f
id = 1
@[property][0]
def f(self, x=[id]):
return x
bin = 2
foo = [bin]

View file

@ -228,3 +228,10 @@ pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSett
pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20178
pub(crate) const fn is_a003_class_scope_shadowing_expansion_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View file

@ -14,6 +14,7 @@ mod tests {
use crate::registry::Rule;
use crate::rules::flake8_builtins;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_resource_path};
use ruff_python_ast::PythonVersion;
@ -63,6 +64,28 @@ mod tests {
Ok(())
}
#[test_case(Rule::BuiltinAttributeShadowing, Path::new("A003.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_builtins").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
flake8_builtins: flake8_builtins::settings::Settings {
strict_checking: true,
..Default::default()
},
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::StdlibModuleShadowing,
Path::new("A005/modules/utils/logging.py"),

View file

@ -6,6 +6,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_a003_class_scope_shadowing_expansion_enabled;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## What it does
@ -123,16 +124,26 @@ pub(crate) fn builtin_attribute_shadowing(
// def repeat(value: int, times: int) -> list[int]:
// return [value] * times
// ```
// In stable, only consider references whose first non-type parent scope is the class
// scope (e.g., decorators, default args, and attribute initializers).
// In preview, also consider references from within the class scope.
let consider_reference = |reference_scope_id: ScopeId| {
if is_a003_class_scope_shadowing_expansion_enabled(checker.settings()) {
if reference_scope_id == scope_id {
return true;
}
}
checker
.semantic()
.first_non_type_parent_scope_id(reference_scope_id)
== Some(scope_id)
};
for reference in binding
.references
.iter()
.map(|reference_id| checker.semantic().reference(*reference_id))
.filter(|reference| {
checker
.semantic()
.first_non_type_parent_scope_id(reference.scope_id())
== Some(scope_id)
})
.filter(|reference| consider_reference(reference.scope_id()))
{
checker.report_diagnostic(
BuiltinAttributeShadowing {

View file

@ -0,0 +1,50 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003 Python builtin is shadowed by method `str` from line 14
--> A003.py:17:31
|
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^
18 | pass
|
A003 Python builtin is shadowed by class attribute `id` from line 3
--> A003.py:20:34
|
18 | pass
19 |
20 | def attribute_usage(self) -> id:
| ^^
21 | pass
|
A003 Python builtin is shadowed by method `property` from line 26
--> A003.py:31:7
|
29 | id = 1
30 |
31 | @[property][0]
| ^^^^^^^^
32 | def f(self, x=[id]):
33 | return x
|
A003 Python builtin is shadowed by class attribute `id` from line 29
--> A003.py:32:20
|
31 | @[property][0]
32 | def f(self, x=[id]):
| ^^
33 | return x
|
A003 Python builtin is shadowed by class attribute `bin` from line 35
--> A003.py:36:12
|
35 | bin = 2
36 | foo = [bin]
| ^^^
|