Move undefined-local into a post-model-building pass (#5928)

## Summary

Similar to #5852 and a bunch of related PRs -- trying to move rules that
rely on point-in-time semantic analysis to _after_ the semantic model
building.
This commit is contained in:
Charlie Marsh 2023-07-20 15:34:22 -04:00 committed by GitHub
parent 2cde9b8aa6
commit bcec2f0c4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 52 deletions

View file

@ -25,3 +25,17 @@ def dec(x):
def f():
dec = 1
return dec
class Class:
def f(self):
print(my_var)
my_var = 1
class Class:
my_var = 0
def f(self):
print(my_var)
my_var = 1

View file

@ -2359,9 +2359,6 @@ where
}
}
ExprContext::Store => {
if self.enabled(Rule::UndefinedLocal) {
pyflakes::rules::undefined_local(self, id);
}
if self.enabled(Rule::NonLowercaseVariableInFunction) {
if self.semantic.scope().kind.is_any_function() {
// Ignore globals.
@ -4383,16 +4380,13 @@ impl<'a> Checker<'a> {
.scopes
.ancestors(scope_id)
.skip(1)
.filter(|scope| scope.kind.is_function() || scope.kind.is_module())
.find_map(|scope| scope.get(name))
{
// Otherwise, if there's an existing binding in a parent scope, mark it as shadowed.
let binding = self.semantic.binding(binding_id);
let shadowed = self.semantic.binding(shadowed_id);
if binding.redefines(shadowed) {
self.semantic
.shadowed_bindings
.insert(binding_id, shadowed_id);
}
self.semantic
.shadowed_bindings
.insert(binding_id, shadowed_id);
}
// Add the binding to the scope.
@ -4878,6 +4872,7 @@ impl<'a> Checker<'a> {
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
Rule::UnusedImport,
Rule::UndefinedLocal,
]) {
return;
}
@ -4924,6 +4919,11 @@ impl<'a> Checker<'a> {
for scope_id in self.deferred.scopes.iter().rev().copied() {
let scope = &self.semantic.scopes[scope_id];
// F823
if self.enabled(Rule::UndefinedLocal) {
pyflakes::rules::undefined_local(self, scope_id, scope, &mut diagnostics);
}
// PLW0602
if self.enabled(Rule::GlobalVariableNotAssigned) {
for (name, binding_id) in scope.bindings() {

View file

@ -2,6 +2,7 @@ use std::string::ToString;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{Scope, ScopeId};
use crate::checkers::ast::Checker;
@ -32,7 +33,7 @@ use crate::checkers::ast::Checker;
/// ```
#[violation]
pub struct UndefinedLocal {
name: String,
pub name: String,
}
impl Violation for UndefinedLocal {
@ -44,55 +45,35 @@ impl Violation for UndefinedLocal {
}
/// F823
pub(crate) fn undefined_local(checker: &mut Checker, name: &str) {
// If the name hasn't already been defined in the current scope...
let current = checker.semantic().scope();
if !current.kind.is_any_function() || current.has(name) {
return;
}
let Some(parent) = current.parent else {
return;
};
// For every function and module scope above us...
let local_access = checker
.semantic()
.scopes
.ancestors(parent)
.find_map(|scope| {
if !(scope.kind.is_any_function() || scope.kind.is_module()) {
return None;
}
// If the name was defined in that scope...
if let Some(binding) = scope
.get(name)
.map(|binding_id| checker.semantic().binding(binding_id))
{
// And has already been accessed in the current scope...
if let Some(range) = binding.references().find_map(|reference_id| {
pub(crate) fn undefined_local(
checker: &Checker,
scope_id: ScopeId,
scope: &Scope,
diagnostics: &mut Vec<Diagnostic>,
) {
if scope.kind.is_any_function() {
for (name, binding_id) in scope.bindings() {
// If the variable shadows a binding in a parent scope...
if let Some(shadowed_id) = checker.semantic().shadowed_binding(binding_id) {
let shadowed = checker.semantic().binding(shadowed_id);
// And that binding was referenced in the current scope...
if let Some(range) = shadowed.references().find_map(|reference_id| {
let reference = checker.semantic().reference(reference_id);
if checker.semantic().is_current_scope(reference.scope_id()) {
if reference.scope_id() == scope_id {
Some(reference.range())
} else {
None
}
}) {
// Then it's probably an error.
return Some(range);
diagnostics.push(Diagnostic::new(
UndefinedLocal {
name: name.to_string(),
},
range,
));
}
}
None
});
if let Some(location) = local_access {
checker.diagnostics.push(Diagnostic::new(
UndefinedLocal {
name: name.to_string(),
},
location,
));
}
}
}

View file

@ -8,4 +8,21 @@ F823.py:6:5: F823 Local variable `my_var` referenced before assignment
| ^^^^^^ F823
|
F823.py:32:15: F823 Local variable `my_var` referenced before assignment
|
30 | class Class:
31 | def f(self):
32 | print(my_var)
| ^^^^^^ F823
33 | my_var = 1
|
F823.py:40:15: F823 Local variable `my_var` referenced before assignment
|
39 | def f(self):
40 | print(my_var)
| ^^^^^^ F823
41 | my_var = 1
|