diff --git a/resources/test/fixtures/pylint/nonlocal_without_binding.py b/resources/test/fixtures/pylint/nonlocal_without_binding.py new file mode 100644 index 0000000000..baa823565b --- /dev/null +++ b/resources/test/fixtures/pylint/nonlocal_without_binding.py @@ -0,0 +1,19 @@ +nonlocal x + + +def f(): + nonlocal x + + +def f(): + nonlocal y + + +def f(): + x = 1 + + def f(): + nonlocal x + + def f(): + nonlocal y diff --git a/src/check_ast.rs b/src/check_ast.rs index 6e349e15e8..5ea0a7edd9 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -305,11 +305,23 @@ where // Mark the binding in the defining scopes as used too. (Skip the global scope // and the current scope.) for name in names { + let mut exists = false; for index in self.scope_stack.iter().skip(1).rev().skip(1) { if let Some(index) = self.scopes[*index].values.get(&name.as_str()) { + exists = true; self.bindings[*index].used = usage; } } + + // Ensure that every nonlocal has an existing binding from a parent scope. + if !exists { + if self.settings.enabled.contains(&CheckCode::PLE0117) { + self.add_check(Check::new( + CheckKind::NonlocalWithoutBinding(name.to_string()), + Range::from_located(stmt), + )); + } + } } } diff --git a/src/checks.rs b/src/checks.rs index 51d7880282..cb4bd016f8 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -99,6 +99,7 @@ pub enum CheckCode { PLC0414, PLC2201, PLC3002, + PLE0117, PLE0118, PLE1142, PLR0206, @@ -642,6 +643,7 @@ pub enum CheckKind { ConsiderUsingFromImport(String, String), GlobalVariableNotAssigned(String), MisplacedComparisonConstant(String), + NonlocalWithoutBinding(String), PropertyWithParameters, UnnecessaryDirectLambdaCall, UseSysExit(String), @@ -954,6 +956,7 @@ impl CheckCode { CheckCode::PLC0414 => CheckKind::UselessImportAlias, CheckCode::PLC2201 => CheckKind::MisplacedComparisonConstant("...".to_string()), CheckCode::PLC3002 => CheckKind::UnnecessaryDirectLambdaCall, + CheckCode::PLE0117 => CheckKind::NonlocalWithoutBinding("...".to_string()), CheckCode::PLE0118 => CheckKind::UsedPriorGlobalDeclaration("...".to_string(), 1), CheckCode::PLE1142 => CheckKind::AwaitOutsideAsync, CheckCode::PLR0402 => { @@ -1408,6 +1411,7 @@ impl CheckCode { CheckCode::PLC0414 => CheckCategory::Pylint, CheckCode::PLC2201 => CheckCategory::Pylint, CheckCode::PLC3002 => CheckCategory::Pylint, + CheckCode::PLE0117 => CheckCategory::Pylint, CheckCode::PLE0118 => CheckCategory::Pylint, CheckCode::PLE1142 => CheckCategory::Pylint, CheckCode::PLR0206 => CheckCategory::Pylint, @@ -1546,6 +1550,7 @@ impl CheckKind { CheckKind::PropertyWithParameters => &CheckCode::PLR0206, CheckKind::UnnecessaryDirectLambdaCall => &CheckCode::PLC3002, CheckKind::UseSysExit(_) => &CheckCode::PLR1722, + CheckKind::NonlocalWithoutBinding(..) => &CheckCode::PLE0117, CheckKind::UsedPriorGlobalDeclaration(..) => &CheckCode::PLE0118, CheckKind::UselessElseOnLoop => &CheckCode::PLW0120, CheckKind::UselessImportAlias => &CheckCode::PLC0414, @@ -1957,6 +1962,9 @@ impl CheckKind { CheckKind::MisplacedComparisonConstant(comparison) => { format!("Comparison should be {comparison}") } + CheckKind::NonlocalWithoutBinding(name) => { + format!("Nonlocal name `{name}` found without binding") + } CheckKind::UnnecessaryDirectLambdaCall => "Lambda expression called directly. Execute \ the expression inline instead." .to_string(), diff --git a/src/checks_gen.rs b/src/checks_gen.rs index 39bf4f7c51..df68dc39fa 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -318,6 +318,7 @@ pub enum CheckCodePrefix { PLE0, PLE01, PLE011, + PLE0117, PLE0118, PLE1, PLE11, @@ -1350,10 +1351,13 @@ impl CheckCodePrefix { CheckCodePrefix::PLC30 => vec![CheckCode::PLC3002], CheckCodePrefix::PLC300 => vec![CheckCode::PLC3002], CheckCodePrefix::PLC3002 => vec![CheckCode::PLC3002], - CheckCodePrefix::PLE => vec![CheckCode::PLE0118, CheckCode::PLE1142], - CheckCodePrefix::PLE0 => vec![CheckCode::PLE0118], - CheckCodePrefix::PLE01 => vec![CheckCode::PLE0118], - CheckCodePrefix::PLE011 => vec![CheckCode::PLE0118], + CheckCodePrefix::PLE => { + vec![CheckCode::PLE0117, CheckCode::PLE0118, CheckCode::PLE1142] + } + CheckCodePrefix::PLE0 => vec![CheckCode::PLE0117, CheckCode::PLE0118], + CheckCodePrefix::PLE01 => vec![CheckCode::PLE0117, CheckCode::PLE0118], + CheckCodePrefix::PLE011 => vec![CheckCode::PLE0117, CheckCode::PLE0118], + CheckCodePrefix::PLE0117 => vec![CheckCode::PLE0117], CheckCodePrefix::PLE0118 => vec![CheckCode::PLE0118], CheckCodePrefix::PLE1 => vec![CheckCode::PLE1142], CheckCodePrefix::PLE11 => vec![CheckCode::PLE1142], @@ -2135,6 +2139,7 @@ impl CheckCodePrefix { CheckCodePrefix::PLE0 => SuffixLength::One, CheckCodePrefix::PLE01 => SuffixLength::Two, CheckCodePrefix::PLE011 => SuffixLength::Three, + CheckCodePrefix::PLE0117 => SuffixLength::Four, CheckCodePrefix::PLE0118 => SuffixLength::Four, CheckCodePrefix::PLE1 => SuffixLength::One, CheckCodePrefix::PLE11 => SuffixLength::Two, diff --git a/src/pylint/mod.rs b/src/pylint/mod.rs index 090b18e734..c188b5b870 100644 --- a/src/pylint/mod.rs +++ b/src/pylint/mod.rs @@ -14,6 +14,7 @@ mod tests { #[test_case(CheckCode::PLC0414, Path::new("import_aliasing.py"); "PLC0414")] #[test_case(CheckCode::PLC2201, Path::new("misplaced_comparison_constant.py"); "PLC2201")] #[test_case(CheckCode::PLC3002, Path::new("unnecessary_direct_lambda_call.py"); "PLC3002")] + #[test_case(CheckCode::PLE0117, Path::new("nonlocal_without_binding.py"); "PLE0117")] #[test_case(CheckCode::PLE0118, Path::new("used_prior_global_declaration.py"); "PLE0118")] #[test_case(CheckCode::PLE1142, Path::new("await_outside_async.py"); "PLE1142")] #[test_case(CheckCode::PLR0206, Path::new("property_with_parameters.py"); "PLR0206")] diff --git a/src/pylint/snapshots/ruff__pylint__tests__PLE0117_nonlocal_without_binding.py.snap b/src/pylint/snapshots/ruff__pylint__tests__PLE0117_nonlocal_without_binding.py.snap new file mode 100644 index 0000000000..304b08259b --- /dev/null +++ b/src/pylint/snapshots/ruff__pylint__tests__PLE0117_nonlocal_without_binding.py.snap @@ -0,0 +1,32 @@ +--- +source: src/pylint/mod.rs +expression: checks +--- +- kind: + NonlocalWithoutBinding: x + location: + row: 5 + column: 4 + end_location: + row: 5 + column: 14 + fix: ~ +- kind: + NonlocalWithoutBinding: y + location: + row: 9 + column: 4 + end_location: + row: 9 + column: 14 + fix: ~ +- kind: + NonlocalWithoutBinding: y + location: + row: 19 + column: 8 + end_location: + row: 19 + column: 18 + fix: ~ +