diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF052.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF052.py new file mode 100644 index 0000000000000..b99ce11108165 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF052.py @@ -0,0 +1,106 @@ +# Correct + +for _ in range(5): + pass + +_valid_type = int + +_valid_var_1: _valid_type + +_valid_var_1 = 1 + +_valid_var_2 = 2 + +_valid_var_3 = _valid_var_1 + _valid_var_2 + +def _valid_fun(): + pass + +_valid_fun() + +def fun(arg): + _valid_unused_var = arg + pass + +_x = "global" + +def fun(): + global _x + return _x + +def fun(): + __dunder__ = "dunder variable" + return __dunder__ + +def fun(): + global _x + _x = "reassigned global" + return _x + +class _ValidClass: + pass + +_ValidClass() + +class ClassOk: + _valid_private_cls_attr = 1 + + print(_valid_private_cls_attr) + + def __init__(self): + self._valid_private_ins_attr = 2 + print(self._valid_private_ins_attr) + + def _valid_method(self): + return self._valid_private_ins_attr + + def method(arg): + _valid_unused_var = arg + return + +def fun(x): + _ = 1 + __ = 2 + ___ = 3 + if x == 1: + return _ + if x == 2: + return __ + if x == 3: + return ___ + return x + +# Incorrect + +class Class_: + def fun(self): + _var = "method variable" # [RUF052] + return _var + +def fun(_var): # [RUF052] + return _var + +def fun(): + _list = "built-in" # [RUF052] + return _list + +x = "global" + +def fun(): + global x + _x = "shadows global" # [RUF052] + return _x + +def foo(): + x = "outer" + def bar(): + nonlocal x + _x = "shadows nonlocal" # [RUF052] + return _x + bar() + return x + +def fun(): + x = "local" + _x = "shadows local" # [RUF052] + return _x diff --git a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs index ed317802f4d35..a871f307e3bed 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs @@ -10,6 +10,7 @@ use crate::rules::{ /// Run lint rules over the [`Binding`]s. pub(crate) fn bindings(checker: &mut Checker) { if !checker.any_enabled(&[ + Rule::AssignmentInAssert, Rule::InvalidAllFormat, Rule::InvalidAllObject, Rule::NonAsciiName, @@ -18,7 +19,7 @@ pub(crate) fn bindings(checker: &mut Checker) { Rule::UnsortedDunderSlots, Rule::UnusedVariable, Rule::UnquotedTypeAlias, - Rule::AssignmentInAssert, + Rule::UsedDummyVariable, ]) { return; } @@ -88,6 +89,11 @@ pub(crate) fn bindings(checker: &mut Checker) { checker.diagnostics.push(diagnostic); } } + if checker.enabled(Rule::UsedDummyVariable) { + if let Some(diagnostic) = ruff::rules::used_dummy_variable(checker, binding) { + checker.diagnostics.push(diagnostic); + } + } if checker.enabled(Rule::AssignmentInAssert) { if let Some(diagnostic) = ruff::rules::assignment_in_assert(checker, binding) { checker.diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index dd9426193701c..d35bf82bbde2e 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -984,6 +984,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument), (Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral), (Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing), + (Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable), (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 870cd5f3eb53a..e5f783db960ae 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -10,6 +10,7 @@ mod tests { use std::path::Path; use anyhow::Result; + use regex::Regex; use rustc_hash::FxHashSet; use test_case::test_case; @@ -70,6 +71,7 @@ mod tests { #[test_case(Rule::InvalidAssertMessageLiteralArgument, Path::new("RUF040.py"))] #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))] #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))] + #[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( @@ -449,4 +451,32 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } + + #[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"^_+", 1)] + #[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"", 2)] + fn custom_regexp_preset( + rule_code: Rule, + path: &Path, + regex_pattern: &str, + id: u8, + ) -> Result<()> { + // Compile the regex from the pattern string + let regex = Regex::new(regex_pattern).unwrap(); + + let snapshot = format!( + "custom_dummy_var_regexp_preset__{}_{}_{}", + rule_code.noqa_code(), + path.to_string_lossy(), + id, + ); + let diagnostics = test_path( + Path::new("ruff").join(path).as_path(), + &settings::LinterSettings { + dummy_variable_rgx: regex, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index a53994e3c928e..69f92b8da2bab 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -38,6 +38,7 @@ pub(crate) use unraw_re_pattern::*; pub(crate) use unsafe_markup_use::*; pub(crate) use unused_async::*; pub(crate) use unused_noqa::*; +pub(crate) use used_dummy_variable::*; pub(crate) use useless_if_else::*; pub(crate) use zip_instead_of_pairwise::*; @@ -85,6 +86,7 @@ mod unraw_re_pattern; mod unsafe_markup_use; mod unused_async; mod unused_noqa; +mod used_dummy_variable; mod useless_if_else; mod zip_instead_of_pairwise; diff --git a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs new file mode 100644 index 0000000000000..d40716dbd36c8 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs @@ -0,0 +1,221 @@ +use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::helpers::is_dunder; +use ruff_python_semantic::{Binding, BindingKind, ScopeId}; +use ruff_python_stdlib::{ + builtins::is_python_builtin, identifiers::is_identifier, keyword::is_keyword, +}; +use ruff_text_size::Ranged; + +use crate::{checkers::ast::Checker, renamer::Renamer}; + +/// ## What it does +/// Checks for accesses of local dummy variables, excluding `_` and dunder variables. +/// +/// By default, "dummy variables" are any variables with names that start with leading +/// underscores. However, this is customisable using the `dummy-variable-rgx` setting). +/// +/// ## Why is this bad? +/// Marking a variable with a leading underscore conveys that it is intentionally unused within the function or method. +/// When these variables are later referenced in the code, it causes confusion and potential misunderstandings about +/// the code's intention. A variable marked as "unused" being subsequently used suggests oversight or unintentional use. +/// This detracts from the clarity and maintainability of the codebase. +/// +/// Sometimes leading underscores are used to avoid variables shadowing other variables, Python builtins, or Python +/// keywords. However, [PEP 8] recommends to use trailing underscores for this rather than leading underscores. +/// +/// ## Example +/// ```python +/// def function(): +/// _variable = 3 +/// return _variable + 1 +/// ``` +/// +/// Use instead: +/// ```python +/// def function(): +/// variable = 3 +/// return variable + 1 +/// ``` +/// +/// ## Fix availability +/// An fix is only available for variables that start with leading underscores. +/// +/// ## Options +/// - [`lint.dummy-variable-rgx`] +/// +/// +/// [PEP 8]: https://peps.python.org/pep-0008/ +#[derive(ViolationMetadata)] +pub(crate) struct UsedDummyVariable { + name: String, + shadowed_kind: Option, +} + +impl Violation for UsedDummyVariable { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + format!("Local dummy variable `{}` is accessed", self.name) + } + + fn fix_title(&self) -> Option { + if let Some(shadowed_kind) = self.shadowed_kind { + return Some(match shadowed_kind { + ShadowedKind::BuiltIn => { + "Prefer using trailing underscores to avoid shadowing a built-in".to_string() + } + ShadowedKind::Keyword => { + "Prefer using trailing underscores to avoid shadowing a keyword".to_string() + } + ShadowedKind::Some => { + "Prefer using trailing underscores to avoid shadowing a variable".to_string() + } + ShadowedKind::None => "Remove leading underscores".to_string(), + }); + } + None + } +} + +/// RUF052 +pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding) -> Option { + let name = binding.name(checker.source()); + + // Ignore `_` and dunder variables + if name == "_" || is_dunder(name) { + return None; + } + // only used variables + if binding.is_unused() { + return None; + } + // Only variables defined via function arguments or assignments. + if !matches!( + binding.kind, + BindingKind::Argument | BindingKind::Assignment + ) { + return None; + } + // This excludes `global` and `nonlocal` variables. + if binding.is_global() || binding.is_nonlocal() { + return None; + } + + let semantic = checker.semantic(); + + // Only variables defined in function scopes + let scope = &semantic.scopes[binding.scope]; + if !scope.kind.is_function() { + return None; + } + if !checker.settings.dummy_variable_rgx.is_match(name) { + return None; + } + + let shadowed_kind = try_shadowed_kind(name, checker, binding.scope); + + let mut diagnostic = Diagnostic::new( + UsedDummyVariable { + name: name.to_string(), + shadowed_kind, + }, + binding.range(), + ); + + // If fix available + if let Some(shadowed_kind) = shadowed_kind { + // Get the possible fix based on the scope + if let Some(fix) = get_possible_fix(name, shadowed_kind, binding.scope, checker) { + diagnostic.try_set_fix(|| { + Renamer::rename(name, &fix, scope, semantic, checker.stylist()) + .map(|(edit, rest)| Fix::safe_edits(edit, rest)) + }); + } + } + + Some(diagnostic) +} + +/// Enumeration of various ways in which a binding can shadow other variables +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum ShadowedKind { + /// The variable shadows a global, nonlocal or local symbol + Some, + /// The variable shadows a builtin symbol + BuiltIn, + /// The variable shadows a keyword + Keyword, + /// The variable does not shadow any other symbols + None, +} + +/// Suggests a potential alternative name to resolve a shadowing conflict. +fn get_possible_fix( + name: &str, + kind: ShadowedKind, + scope_id: ScopeId, + checker: &Checker, +) -> Option { + // Remove leading underscores for processing + let trimmed_name = name.trim_start_matches('_'); + + // Construct the potential fix name based on ShadowedKind + let fix_name = match kind { + ShadowedKind::Some | ShadowedKind::BuiltIn | ShadowedKind::Keyword => { + format!("{trimmed_name}_") // Append an underscore + } + ShadowedKind::None => trimmed_name.to_string(), + }; + + // Check if the fix name is again dummy identifier + if checker.settings.dummy_variable_rgx.is_match(&fix_name) { + return None; + } + + // Ensure the fix name is not already taken in the scope or enclosing scopes + if !checker + .semantic() + .is_available_in_scope(&fix_name, scope_id) + { + return None; + } + + // Check if the fix name is a valid identifier + is_identifier(&fix_name).then_some(fix_name) +} + +/// Determines the kind of shadowing or conflict for a given variable name. +fn try_shadowed_kind(name: &str, checker: &Checker, scope_id: ScopeId) -> Option { + // If the name starts with an underscore, we don't consider it + if !name.starts_with('_') { + return None; + } + + // Trim the leading underscores for further checks + let trimmed_name = name.trim_start_matches('_'); + + // Check the kind in order of precedence + if is_keyword(trimmed_name) { + return Some(ShadowedKind::Keyword); + } + + if is_python_builtin( + trimmed_name, + checker.settings.target_version.minor(), + checker.source_type.is_ipynb(), + ) { + return Some(ShadowedKind::BuiltIn); + } + + if !checker + .semantic() + .is_available_in_scope(trimmed_name, scope_id) + { + return Some(ShadowedKind::Some); + } + + // Default to no shadowing + Some(ShadowedKind::None) +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF052_RUF052.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF052_RUF052.py.snap new file mode 100644 index 0000000000000..ad8e5355311b1 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF052_RUF052.py.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF052.py:77:9: RUF052 [*] Local dummy variable `_var` is accessed + | +75 | class Class_: +76 | def fun(self): +77 | _var = "method variable" # [RUF052] + | ^^^^ RUF052 +78 | return _var + | + = help: Remove leading underscores + +ℹ Safe fix +74 74 | +75 75 | class Class_: +76 76 | def fun(self): +77 |- _var = "method variable" # [RUF052] +78 |- return _var + 77 |+ var = "method variable" # [RUF052] + 78 |+ return var +79 79 | +80 80 | def fun(_var): # [RUF052] +81 81 | return _var + +RUF052.py:80:9: RUF052 [*] Local dummy variable `_var` is accessed + | +78 | return _var +79 | +80 | def fun(_var): # [RUF052] + | ^^^^ RUF052 +81 | return _var + | + = help: Remove leading underscores + +ℹ Safe fix +77 77 | _var = "method variable" # [RUF052] +78 78 | return _var +79 79 | +80 |-def fun(_var): # [RUF052] +81 |- return _var + 80 |+def fun(var): # [RUF052] + 81 |+ return var +82 82 | +83 83 | def fun(): +84 84 | _list = "built-in" # [RUF052] + +RUF052.py:84:5: RUF052 [*] Local dummy variable `_list` is accessed + | +83 | def fun(): +84 | _list = "built-in" # [RUF052] + | ^^^^^ RUF052 +85 | return _list + | + = help: Prefer using trailing underscores to avoid shadowing a built-in + +ℹ Safe fix +81 81 | return _var +82 82 | +83 83 | def fun(): +84 |- _list = "built-in" # [RUF052] +85 |- return _list + 84 |+ list_ = "built-in" # [RUF052] + 85 |+ return list_ +86 86 | +87 87 | x = "global" +88 88 | + +RUF052.py:91:5: RUF052 [*] Local dummy variable `_x` is accessed + | +89 | def fun(): +90 | global x +91 | _x = "shadows global" # [RUF052] + | ^^ RUF052 +92 | return _x + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +ℹ Safe fix +88 88 | +89 89 | def fun(): +90 90 | global x +91 |- _x = "shadows global" # [RUF052] +92 |- return _x + 91 |+ x_ = "shadows global" # [RUF052] + 92 |+ return x_ +93 93 | +94 94 | def foo(): +95 95 | x = "outer" + +RUF052.py:98:5: RUF052 [*] Local dummy variable `_x` is accessed + | + 96 | def bar(): + 97 | nonlocal x + 98 | _x = "shadows nonlocal" # [RUF052] + | ^^ RUF052 + 99 | return _x +100 | bar() + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +ℹ Safe fix +95 95 | x = "outer" +96 96 | def bar(): +97 97 | nonlocal x +98 |- _x = "shadows nonlocal" # [RUF052] +99 |- return _x + 98 |+ x_ = "shadows nonlocal" # [RUF052] + 99 |+ return x_ +100 100 | bar() +101 101 | return x +102 102 | + +RUF052.py:105:5: RUF052 [*] Local dummy variable `_x` is accessed + | +103 | def fun(): +104 | x = "local" +105 | _x = "shadows local" # [RUF052] + | ^^ RUF052 +106 | return _x + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +ℹ Safe fix +102 102 | +103 103 | def fun(): +104 104 | x = "local" +105 |- _x = "shadows local" # [RUF052] +106 |- return _x + 105 |+ x_ = "shadows local" # [RUF052] + 106 |+ return x_ diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_1.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_1.snap new file mode 100644 index 0000000000000..ad8e5355311b1 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_1.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF052.py:77:9: RUF052 [*] Local dummy variable `_var` is accessed + | +75 | class Class_: +76 | def fun(self): +77 | _var = "method variable" # [RUF052] + | ^^^^ RUF052 +78 | return _var + | + = help: Remove leading underscores + +ℹ Safe fix +74 74 | +75 75 | class Class_: +76 76 | def fun(self): +77 |- _var = "method variable" # [RUF052] +78 |- return _var + 77 |+ var = "method variable" # [RUF052] + 78 |+ return var +79 79 | +80 80 | def fun(_var): # [RUF052] +81 81 | return _var + +RUF052.py:80:9: RUF052 [*] Local dummy variable `_var` is accessed + | +78 | return _var +79 | +80 | def fun(_var): # [RUF052] + | ^^^^ RUF052 +81 | return _var + | + = help: Remove leading underscores + +ℹ Safe fix +77 77 | _var = "method variable" # [RUF052] +78 78 | return _var +79 79 | +80 |-def fun(_var): # [RUF052] +81 |- return _var + 80 |+def fun(var): # [RUF052] + 81 |+ return var +82 82 | +83 83 | def fun(): +84 84 | _list = "built-in" # [RUF052] + +RUF052.py:84:5: RUF052 [*] Local dummy variable `_list` is accessed + | +83 | def fun(): +84 | _list = "built-in" # [RUF052] + | ^^^^^ RUF052 +85 | return _list + | + = help: Prefer using trailing underscores to avoid shadowing a built-in + +ℹ Safe fix +81 81 | return _var +82 82 | +83 83 | def fun(): +84 |- _list = "built-in" # [RUF052] +85 |- return _list + 84 |+ list_ = "built-in" # [RUF052] + 85 |+ return list_ +86 86 | +87 87 | x = "global" +88 88 | + +RUF052.py:91:5: RUF052 [*] Local dummy variable `_x` is accessed + | +89 | def fun(): +90 | global x +91 | _x = "shadows global" # [RUF052] + | ^^ RUF052 +92 | return _x + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +ℹ Safe fix +88 88 | +89 89 | def fun(): +90 90 | global x +91 |- _x = "shadows global" # [RUF052] +92 |- return _x + 91 |+ x_ = "shadows global" # [RUF052] + 92 |+ return x_ +93 93 | +94 94 | def foo(): +95 95 | x = "outer" + +RUF052.py:98:5: RUF052 [*] Local dummy variable `_x` is accessed + | + 96 | def bar(): + 97 | nonlocal x + 98 | _x = "shadows nonlocal" # [RUF052] + | ^^ RUF052 + 99 | return _x +100 | bar() + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +ℹ Safe fix +95 95 | x = "outer" +96 96 | def bar(): +97 97 | nonlocal x +98 |- _x = "shadows nonlocal" # [RUF052] +99 |- return _x + 98 |+ x_ = "shadows nonlocal" # [RUF052] + 99 |+ return x_ +100 100 | bar() +101 101 | return x +102 102 | + +RUF052.py:105:5: RUF052 [*] Local dummy variable `_x` is accessed + | +103 | def fun(): +104 | x = "local" +105 | _x = "shadows local" # [RUF052] + | ^^ RUF052 +106 | return _x + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +ℹ Safe fix +102 102 | +103 103 | def fun(): +104 104 | x = "local" +105 |- _x = "shadows local" # [RUF052] +106 |- return _x + 105 |+ x_ = "shadows local" # [RUF052] + 106 |+ return x_ diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_2.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_2.snap new file mode 100644 index 0000000000000..ea20bafed686e --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_2.snap @@ -0,0 +1,121 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF052.py:21:9: RUF052 Local dummy variable `arg` is accessed + | +19 | _valid_fun() +20 | +21 | def fun(arg): + | ^^^ RUF052 +22 | _valid_unused_var = arg +23 | pass + | + +RUF052.py:50:18: RUF052 Local dummy variable `self` is accessed + | +48 | print(_valid_private_cls_attr) +49 | +50 | def __init__(self): + | ^^^^ RUF052 +51 | self._valid_private_ins_attr = 2 +52 | print(self._valid_private_ins_attr) + | + +RUF052.py:54:23: RUF052 Local dummy variable `self` is accessed + | +52 | print(self._valid_private_ins_attr) +53 | +54 | def _valid_method(self): + | ^^^^ RUF052 +55 | return self._valid_private_ins_attr + | + +RUF052.py:57:16: RUF052 Local dummy variable `arg` is accessed + | +55 | return self._valid_private_ins_attr +56 | +57 | def method(arg): + | ^^^ RUF052 +58 | _valid_unused_var = arg +59 | return + | + +RUF052.py:61:9: RUF052 Local dummy variable `x` is accessed + | +59 | return +60 | +61 | def fun(x): + | ^ RUF052 +62 | _ = 1 +63 | __ = 2 + | + +RUF052.py:77:9: RUF052 Local dummy variable `_var` is accessed + | +75 | class Class_: +76 | def fun(self): +77 | _var = "method variable" # [RUF052] + | ^^^^ RUF052 +78 | return _var + | + = help: Remove leading underscores + +RUF052.py:80:9: RUF052 Local dummy variable `_var` is accessed + | +78 | return _var +79 | +80 | def fun(_var): # [RUF052] + | ^^^^ RUF052 +81 | return _var + | + = help: Remove leading underscores + +RUF052.py:84:5: RUF052 Local dummy variable `_list` is accessed + | +83 | def fun(): +84 | _list = "built-in" # [RUF052] + | ^^^^^ RUF052 +85 | return _list + | + = help: Prefer using trailing underscores to avoid shadowing a built-in + +RUF052.py:91:5: RUF052 Local dummy variable `_x` is accessed + | +89 | def fun(): +90 | global x +91 | _x = "shadows global" # [RUF052] + | ^^ RUF052 +92 | return _x + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +RUF052.py:95:3: RUF052 Local dummy variable `x` is accessed + | +94 | def foo(): +95 | x = "outer" + | ^ RUF052 +96 | def bar(): +97 | nonlocal x + | + +RUF052.py:98:5: RUF052 Local dummy variable `_x` is accessed + | + 96 | def bar(): + 97 | nonlocal x + 98 | _x = "shadows nonlocal" # [RUF052] + | ^^ RUF052 + 99 | return _x +100 | bar() + | + = help: Prefer using trailing underscores to avoid shadowing a variable + +RUF052.py:105:5: RUF052 Local dummy variable `_x` is accessed + | +103 | def fun(): +104 | x = "local" +105 | _x = "shadows local" # [RUF052] + | ^^ RUF052 +106 | return _x + | + = help: Prefer using trailing underscores to avoid shadowing a variable diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index b5d7e993ec201..f602c763046fd 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -325,9 +325,15 @@ impl<'a> SemanticModel<'a> { } /// Return `true` if `member` is an "available" symbol, i.e., a symbol that has not been bound - /// in the current scope, or in any containing scope. + /// in the current scope currently being visited, or in any containing scope. pub fn is_available(&self, member: &str) -> bool { - self.lookup_symbol(member) + self.is_available_in_scope(member, self.scope_id) + } + + /// Return `true` if `member` is an "available" symbol in a given scope, i.e., + /// a symbol that has not been bound in that current scope, or in any containing scope. + pub fn is_available_in_scope(&self, member: &str, scope_id: ScopeId) -> bool { + self.lookup_symbol_in_scope(member, scope_id, false) .map(|binding_id| &self.bindings[binding_id]) .map_or(true, |binding| binding.kind.is_builtin()) } @@ -620,10 +626,22 @@ impl<'a> SemanticModel<'a> { } } - /// Lookup a symbol in the current scope. This is a carbon copy of [`Self::resolve_load`], but - /// doesn't add any read references to the resolved symbol. + /// Lookup a symbol in the current scope. pub fn lookup_symbol(&self, symbol: &str) -> Option { - if self.in_forward_reference() { + self.lookup_symbol_in_scope(symbol, self.scope_id, self.in_forward_reference()) + } + + /// Lookup a symbol in a certain scope + /// + /// This is a carbon copy of [`Self::resolve_load`], but + /// doesn't add any read references to the resolved symbol. + pub fn lookup_symbol_in_scope( + &self, + symbol: &str, + scope_id: ScopeId, + in_forward_reference: bool, + ) -> Option { + if in_forward_reference { if let Some(binding_id) = self.scopes.global().get(symbol) { if !self.bindings[binding_id].is_unbound() { return Some(binding_id); @@ -633,7 +651,7 @@ impl<'a> SemanticModel<'a> { let mut seen_function = false; let mut class_variables_visible = true; - for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() { + for (index, scope_id) in self.scopes.ancestor_ids(scope_id).enumerate() { let scope = &self.scopes[scope_id]; if scope.kind.is_class() { if seen_function && matches!(symbol, "__class__") { diff --git a/crates/ruff_python_stdlib/src/keyword.rs b/crates/ruff_python_stdlib/src/keyword.rs index 7f361c0b6988f..6057a0d9cf1dc 100644 --- a/crates/ruff_python_stdlib/src/keyword.rs +++ b/crates/ruff_python_stdlib/src/keyword.rs @@ -1,5 +1,5 @@ // See: https://github.com/python/cpython/blob/9d692841691590c25e6cf5b2250a594d3bf54825/Lib/keyword.py#L18 -pub(crate) fn is_keyword(name: &str) -> bool { +pub fn is_keyword(name: &str) -> bool { matches!( name, "False" diff --git a/ruff.schema.json b/ruff.schema.json index 29c1a05ecfaa1..72ff32f77bace 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3845,6 +3845,7 @@ "RUF041", "RUF048", "RUF05", + "RUF052", "RUF055", "RUF1", "RUF10",