diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py b/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py index b2bb8b872f..c7db608f2d 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py @@ -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] diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 2459fd1afc..be0a925fc7 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -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() +} diff --git a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs index 6f863dc2e3..315ab704e7 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs @@ -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"), diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index b0e5c4979c..948e0c892d 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -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 { diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__preview__A003_A003.py.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__preview__A003_A003.py.snap new file mode 100644 index 0000000000..6fd8c6f961 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__preview__A003_A003.py.snap @@ -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] + | ^^^ + |