diff --git a/resources/test/fixtures/F841.py b/resources/test/fixtures/F841.py index feae495fc9..6626be74ab 100644 --- a/resources/test/fixtures/F841.py +++ b/resources/test/fixtures/F841.py @@ -10,13 +10,13 @@ except ValueError as e: print(e) -def f(): +def f1(): x = 1 y = 2 z = x + y -def g(): +def f2(): foo = (1, 2) (a, b) = (1, 2) @@ -26,6 +26,12 @@ def g(): (x, y) = baz = bar -def h(): +def f3(): locals() x = 1 + + +def f4(): + _ = 1 + __ = 1 + _discarded = 1 diff --git a/src/ast/checks.rs b/src/ast/checks.rs index 466cd29cd2..19baa9d028 100644 --- a/src/ast/checks.rs +++ b/src/ast/checks.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use itertools::izip; +use regex::Regex; use rustpython_parser::ast::{ Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, Location, Stmt, StmtKind, Unaryop, @@ -71,7 +72,11 @@ pub fn check_not_tests( } /// Check UnusedVariable compliance. -pub fn check_unused_variables(scope: &Scope, locator: &dyn CheckLocator) -> Vec { +pub fn check_unused_variables( + scope: &Scope, + locator: &dyn CheckLocator, + dummy_variable_rgx: &Regex, +) -> Vec { let mut checks: Vec = vec![]; if matches!( @@ -82,13 +87,12 @@ pub fn check_unused_variables(scope: &Scope, locator: &dyn CheckLocator) -> Vec< } for (name, binding) in scope.values.iter() { - // TODO(charlie): Ignore if using `locals`. if binding.used.is_none() - && name != "_" + && matches!(binding.kind, BindingKind::Assignment) + && !dummy_variable_rgx.is_match(name) && name != "__tracebackhide__" && name != "__traceback_info__" && name != "__traceback_supplement__" - && matches!(binding.kind, BindingKind::Assignment) { checks.push(Check::new( CheckKind::UnusedVariable(name.to_string()), diff --git a/src/check_ast.rs b/src/check_ast.rs index 67aca0f2e8..307c6490ef 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -1415,8 +1415,11 @@ impl<'a> Checker<'a> { fn check_deferred_assignments(&mut self) { if self.settings.select.contains(&CheckCode::F841) { while let Some(index) = self.deferred_assignments.pop() { - self.checks - .extend(checks::check_unused_variables(&self.scopes[index], self)); + self.checks.extend(checks::check_unused_variables( + &self.scopes[index], + self, + &self.settings.dummy_variable_rgx, + )); } } } diff --git a/src/check_lines.rs b/src/check_lines.rs index 7a8e3109e2..81916ed436 100644 --- a/src/check_lines.rs +++ b/src/check_lines.rs @@ -182,6 +182,8 @@ pub fn check_lines( mod tests { use std::collections::BTreeSet; + use regex::Regex; + use super::check_lines; use super::*; @@ -198,6 +200,7 @@ mod tests { exclude: vec![], extend_exclude: vec![], select: BTreeSet::from_iter(vec![CheckCode::E501]), + dummy_variable_rgx: Regex::new(r"^_+").unwrap(), }; check_lines( &mut checks, diff --git a/src/linter.rs b/src/linter.rs index a10b87f3e7..f0176dcc63 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -119,6 +119,7 @@ mod tests { use std::path::Path; use anyhow::Result; + use regex::Regex; use rustpython_parser::lexer; use rustpython_parser::lexer::LexResult; @@ -596,6 +597,21 @@ mod tests { Ok(()) } + #[test] + fn f841_dummy_variable_rgx() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/F841.py"), + &settings::Settings { + dummy_variable_rgx: Regex::new(r"^z$").unwrap(), + ..settings::Settings::for_rule(CheckCode::F841) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + #[test] fn f901() -> Result<()> { let mut checks = check_path( diff --git a/src/main.rs b/src/main.rs index 87194b15d5..fcf92bcb92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ extern crate core; +use regex::Regex; use std::io; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -85,6 +86,9 @@ struct Cli { /// Enable automatic additions of noqa directives to failing lines. #[clap(long, action)] add_noqa: bool, + /// Regular expression matching the name of dummy variables. + #[clap(long)] + dummy_variable_rgx: Option, } #[cfg(feature = "update-informer")] @@ -265,6 +269,9 @@ fn inner_main() -> Result { if !cli.extend_ignore.is_empty() { settings.ignore(&cli.extend_ignore); } + if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx { + settings.dummy_variable_rgx = dummy_variable_rgx; + } if cli.show_settings && cli.show_files { eprintln!("Error: specify --show-settings or show-files (not both)."); diff --git a/src/pyproject.rs b/src/pyproject.rs index e70897edb3..588cbdda7e 100644 --- a/src/pyproject.rs +++ b/src/pyproject.rs @@ -30,6 +30,7 @@ pub struct Config { pub extend_exclude: Option>, pub select: Option>, pub ignore: Option>, + pub dummy_variable_rgx: Option, } #[derive(Debug, PartialEq, Eq, Deserialize)] @@ -130,6 +131,7 @@ mod tests { extend_exclude: None, select: None, ignore: None, + dummy_variable_rgx: None, }) }) ); @@ -150,6 +152,7 @@ line-length = 79 extend_exclude: None, select: None, ignore: None, + dummy_variable_rgx: None, }) }) ); @@ -170,6 +173,7 @@ exclude = ["foo.py"] extend_exclude: None, select: None, ignore: None, + dummy_variable_rgx: None, }) }) ); @@ -190,6 +194,7 @@ select = ["E501"] extend_exclude: None, select: Some(vec![CheckCode::E501]), ignore: None, + dummy_variable_rgx: None, }) }) ); @@ -210,6 +215,7 @@ ignore = ["E501"] extend_exclude: None, select: None, ignore: Some(vec![CheckCode::E501]), + dummy_variable_rgx: None, }) }) ); @@ -274,6 +280,7 @@ other-attribute = 1 ]), select: None, ignore: None, + dummy_variable_rgx: None, } ); diff --git a/src/settings.rs b/src/settings.rs index 57249c9ff7..c0ed7cbbd7 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,9 +2,10 @@ use std::collections::BTreeSet; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use glob::Pattern; use once_cell::sync::Lazy; +use regex::Regex; use crate::checks::{CheckCode, DEFAULT_CHECK_CODES}; use crate::fs; @@ -43,6 +44,7 @@ pub struct Settings { pub exclude: Vec, pub extend_exclude: Vec, pub select: BTreeSet, + pub dummy_variable_rgx: Regex, } impl Settings { @@ -54,6 +56,7 @@ impl Settings { exclude: vec![], extend_exclude: vec![], select: BTreeSet::from([check_code]), + dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(), } } @@ -65,6 +68,7 @@ impl Settings { exclude: vec![], extend_exclude: vec![], select: BTreeSet::from_iter(check_codes), + dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(), } } } @@ -72,6 +76,7 @@ impl Settings { impl Hash for Settings { fn hash(&self, state: &mut H) { self.line_length.hash(state); + self.dummy_variable_rgx.as_str().hash(state); for value in self.select.iter() { value.hash(state); } @@ -102,6 +107,9 @@ static DEFAULT_EXCLUDE: Lazy> = Lazy::new(|| { ] }); +static DEFAULT_DUMMY_VARIABLE_RGX: Lazy = + Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap()); + impl Settings { pub fn from_pyproject( pyproject: Option, @@ -133,6 +141,11 @@ impl Settings { } else { BTreeSet::from_iter(DEFAULT_CHECK_CODES) }, + dummy_variable_rgx: match config.dummy_variable_rgx { + Some(pattern) => Regex::new(&pattern) + .map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?, + None => DEFAULT_DUMMY_VARIABLE_RGX.clone(), + }, pyproject, project_root, }; diff --git a/src/snapshots/ruff__linter__tests__f841_dummy_variable_rgx.snap b/src/snapshots/ruff__linter__tests__f841_dummy_variable_rgx.snap new file mode 100644 index 0000000000..d3d9b061ec --- /dev/null +++ b/src/snapshots/ruff__linter__tests__f841_dummy_variable_rgx.snap @@ -0,0 +1,47 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: + UnusedVariable: e + location: + row: 3 + column: 1 + fix: ~ +- kind: + UnusedVariable: foo + location: + row: 20 + column: 5 + fix: ~ +- kind: + UnusedVariable: a + location: + row: 21 + column: 6 + fix: ~ +- kind: + UnusedVariable: b + location: + row: 21 + column: 9 + fix: ~ +- kind: + UnusedVariable: _ + location: + row: 35 + column: 5 + fix: ~ +- kind: + UnusedVariable: __ + location: + row: 36 + column: 5 + fix: ~ +- kind: + UnusedVariable: _discarded + location: + row: 37 + column: 5 + fix: ~ +