Move shadow analysis later

This commit is contained in:
Charlie Marsh 2023-06-03 22:42:28 -04:00
parent c2a3e97b7f
commit f3e83d74bb
5 changed files with 207 additions and 100 deletions

View File

@ -4342,98 +4342,13 @@ impl<'a> Checker<'a> {
// Create the `Binding`. // Create the `Binding`.
let binding_id = self.semantic_model.push_binding(range, kind, flags); let binding_id = self.semantic_model.push_binding(range, kind, flags);
let binding = &self.semantic_model.bindings[binding_id];
// Determine whether the binding shadows any existing bindings. // Add the `Binding` to the current scope.
if let Some((stack_index, shadowed_id)) = self
.semantic_model
.scopes
.ancestors(self.semantic_model.scope_id)
.enumerate()
.find_map(|(stack_index, scope)| {
scope.get(name).map(|binding_id| (stack_index, binding_id))
})
{
let shadowed = &self.semantic_model.bindings[shadowed_id];
let in_current_scope = stack_index == 0;
if !shadowed.kind.is_builtin()
&& shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
!branch_detection::different_forks(left, right, &self.semantic_model.stmts)
})
})
{
let shadows_import = matches!(
shadowed.kind,
BindingKind::Importation(..)
| BindingKind::FromImportation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::FutureImportation
);
if binding.kind.is_loop_var() && shadows_import {
if self.enabled(Rule::ImportShadowedByLoopVar) {
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
));
}
} else if in_current_scope {
if !shadowed.is_used()
&& binding.redefines(shadowed)
&& (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import)
&& !(shadowed.kind.is_function_definition()
&& analyze::visibility::is_overload(
&self.semantic_model,
cast::decorator_list(
self.semantic_model.stmts[shadowed.source.unwrap()],
),
))
{
if self.enabled(Rule::RedefinedWhileUnused) {
#[allow(deprecated)]
let line = self.locator.compute_line_index(
shadowed
.trimmed_range(&self.semantic_model, self.locator)
.start(),
);
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: name.to_string(),
line,
},
binding.trimmed_range(&self.semantic_model, self.locator),
);
if let Some(parent) = binding.source {
let parent = self.semantic_model.stmts[parent];
if parent.is_import_from_stmt()
&& parent.range().contains_range(binding.range)
{
diagnostic.set_parent(parent.start());
}
}
self.diagnostics.push(diagnostic);
}
}
} else if shadows_import && binding.redefines(shadowed) {
self.semantic_model
.shadowed_bindings
.insert(binding_id, shadowed_id);
}
}
}
// If there's an existing binding in this scope, copy its references.
if let Some(shadowed) = self.semantic_model.scopes[scope_id] if let Some(shadowed) = self.semantic_model.scopes[scope_id]
.get(name) .get(name)
.map(|binding_id| &self.semantic_model.bindings[binding_id]) .map(|binding_id| &self.semantic_model.bindings[binding_id])
{ {
// If there's an existing binding in this scope, copy its references.
match &shadowed.kind { match &shadowed.kind {
BindingKind::Builtin => { BindingKind::Builtin => {
// Avoid overriding builtins. // Avoid overriding builtins.
@ -4459,6 +4374,17 @@ impl<'a> Checker<'a> {
{ {
return binding_id; return binding_id;
} }
} else if let Some(shadowed_id) = self
.semantic_model
.scopes
.ancestors(scope_id)
.skip(1)
.find_map(|scope| scope.get(name))
{
// Otherwise, if there's an existing binding in a parent scope, mark it as shadowed.
self.semantic_model
.shadowed_bindings
.insert(binding_id, shadowed_id);
} }
// Add the binding to the scope. // Add the binding to the scope.
@ -4477,7 +4403,7 @@ impl<'a> Checker<'a> {
{ {
// Add the builtin to the scope. // Add the builtin to the scope.
let binding_id = self.semantic_model.push_builtin(); let binding_id = self.semantic_model.push_builtin();
let scope = self.semantic_model.scope_mut(); let scope = self.semantic_model.global_scope_mut();
scope.add(builtin, binding_id); scope.add(builtin, binding_id);
} }
} }
@ -4889,10 +4815,11 @@ impl<'a> Checker<'a> {
if !(enforce_typing_imports if !(enforce_typing_imports
|| self.any_enabled(&[ || self.any_enabled(&[
Rule::UnusedImport, Rule::ImportShadowedByLoopVar,
Rule::UndefinedLocalWithImportStarUsage,
Rule::RedefinedWhileUnused, Rule::RedefinedWhileUnused,
Rule::UndefinedExport, Rule::UndefinedExport,
Rule::UndefinedLocalWithImportStarUsage,
Rule::UnusedImport,
])) ]))
{ {
return; return;
@ -4953,8 +4880,8 @@ impl<'a> Checker<'a> {
}; };
let mut diagnostics: Vec<Diagnostic> = vec![]; let mut diagnostics: Vec<Diagnostic> = vec![];
for scope_id in self.semantic_model.dead_scopes.iter().rev() { for scope_id in self.semantic_model.dead_scopes.iter().rev().copied() {
let scope = &self.semantic_model.scopes[*scope_id]; let scope = &self.semantic_model.scopes[scope_id];
if scope.kind.is_module() { if scope.kind.is_module() {
// F822 // F822
@ -5013,22 +4940,135 @@ impl<'a> Checker<'a> {
} }
} }
// F402
if self.enabled(Rule::ImportShadowedByLoopVar) {
for (name, binding_id) in scope.bindings() {
for shadow in self.semantic_model.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding isn't a loop variable, abort.
let binding = &self.semantic_model.bindings[shadow.binding_id()];
if !binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding isn't an import, abort.
let shadowed = &self.semantic_model.bindings[shadow.shadowed_id()];
if !matches!(
shadowed.kind,
BindingKind::Importation(..)
| BindingKind::FromImportation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::FutureImportation
) {
continue;
}
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(
left,
right,
&self.semantic_model.stmts,
)
})
}) {
continue;
}
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
));
}
}
}
// Imports in classes are public members. // Imports in classes are public members.
if scope.kind.is_class() { if scope.kind.is_class() {
continue; continue;
} }
// Look for any bindings that were redefined in another scope, and remain // F811
// unused. Note that we only store references in `shadowed_bindings` if
// the bindings are in different scopes.
if self.enabled(Rule::RedefinedWhileUnused) { if self.enabled(Rule::RedefinedWhileUnused) {
for (name, binding_id) in scope.bindings() { for (name, binding_id) in scope.bindings() {
if let Some(shadowed) = self.semantic_model.shadowed_binding(binding_id) { for shadow in self.semantic_model.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding is a loop variable, abort, to avoid overlap
// with F402.
let binding = &self.semantic_model.bindings[shadow.binding_id()];
if binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding is used, abort.
let shadowed = &self.semantic_model.bindings[shadow.shadowed_id()];
if shadowed.is_used() { if shadowed.is_used() {
continue; continue;
} }
let binding = &self.semantic_model.bindings[binding_id]; // If the shadowing binding isn't considered a "redifinition" of the
// shadowed binding, abort.
if !binding.redefines(shadowed) {
continue;
}
if shadow.same_scope() {
// If the symbol is a dummy variable, abort, unless the shadowed
// binding is an import.
if !matches!(
shadowed.kind,
BindingKind::Importation(..)
| BindingKind::FromImportation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::FutureImportation
) && self.settings.dummy_variable_rgx.is_match(name)
{
continue;
}
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& analyze::visibility::is_overload(
&self.semantic_model,
cast::decorator_list(
self.semantic_model.stmts[shadowed.source.unwrap()],
),
)
{
continue;
}
} else {
// Only enforce cross-scope shadowing for imports.
if !matches!(
shadowed.kind,
BindingKind::Importation(..)
| BindingKind::FromImportation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::FutureImportation
) {
continue;
}
}
// If the bindings are in different forks, abort.
if shadowed.kind.is_builtin()
|| shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(
left,
right,
&self.semantic_model.stmts,
)
})
})
{
continue;
}
#[allow(deprecated)] #[allow(deprecated)]
let line = self.locator.compute_line_index( let line = self.locator.compute_line_index(
@ -5063,7 +5103,7 @@ impl<'a> Checker<'a> {
} else { } else {
self.semantic_model self.semantic_model
.scopes .scopes
.ancestor_ids(*scope_id) .ancestor_ids(scope_id)
.flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter()) .flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter())
.copied() .copied()
.collect() .collect()

View File

@ -1890,7 +1890,7 @@ mod tests {
try: pass try: pass
except Exception as fu: pass except Exception as fu: pass
"#, "#,
&[Rule::RedefinedWhileUnused, Rule::UnusedVariable], &[Rule::UnusedVariable, Rule::RedefinedWhileUnused],
); );
} }

View File

@ -94,7 +94,6 @@ impl<'a> Binding<'a> {
existing.kind, existing.kind,
BindingKind::ClassDefinition BindingKind::ClassDefinition
| BindingKind::FunctionDefinition | BindingKind::FunctionDefinition
| BindingKind::Builtin
| BindingKind::Importation(..) | BindingKind::Importation(..)
| BindingKind::FromImportation(..) | BindingKind::FromImportation(..)
| BindingKind::SubmoduleImportation(..) | BindingKind::SubmoduleImportation(..)

View File

@ -805,6 +805,69 @@ impl<'a> SemanticModel<'a> {
pub const fn future_annotations(&self) -> bool { pub const fn future_annotations(&self) -> bool {
self.flags.contains(SemanticModelFlags::FUTURE_ANNOTATIONS) self.flags.contains(SemanticModelFlags::FUTURE_ANNOTATIONS)
} }
/// Return an iterator over all bindings shadowed by the given [`BindingId`], within the
/// containing scope, and across scopes.
pub fn shadowed_bindings(
&self,
scope_id: ScopeId,
binding_id: BindingId,
) -> impl Iterator<Item = ShadowedBinding> + '_ {
let mut first = true;
let mut binding_id = binding_id;
std::iter::from_fn(move || {
// First, check whether this binding is shadowing another binding in a different scope.
if std::mem::take(&mut first) {
if let Some(shadowed_id) = self.shadowed_bindings.get(&binding_id).copied() {
return Some(ShadowedBinding {
binding_id,
shadowed_id,
same_scope: false,
});
}
}
// Otherwise, check whether this binding is shadowing another binding in the same scope.
if let Some(shadowed_id) = self.scopes[scope_id].shadowed_binding(binding_id) {
let next = ShadowedBinding {
binding_id,
shadowed_id,
same_scope: true,
};
// Advance to the next binding in the scope.
first = true;
binding_id = shadowed_id;
return Some(next);
}
None
})
}
}
pub struct ShadowedBinding {
/// The binding that is shadowing another binding.
binding_id: BindingId,
/// The binding that is being shadowed.
shadowed_id: BindingId,
/// Whether the shadowing and shadowed bindings are in the same scope.
same_scope: bool,
}
impl ShadowedBinding {
pub const fn binding_id(&self) -> BindingId {
self.binding_id
}
pub const fn shadowed_id(&self) -> BindingId {
self.shadowed_id
}
pub const fn same_scope(&self) -> bool {
self.same_scope
}
} }
bitflags! { bitflags! {

View File

@ -87,6 +87,11 @@ impl<'a> Scope<'a> {
self.bindings.iter().map(|(&name, &id)| (name, id)) self.bindings.iter().map(|(&name, &id)| (name, id))
} }
/// Returns a tuple of the name and id of all bindings defined in this scope.
pub fn shadowed_binding(&self, id: BindingId) -> Option<BindingId> {
self.shadowed_bindings.get(&id).copied()
}
/// Returns an iterator over all [bindings](BindingId) bound to the given name, including /// Returns an iterator over all [bindings](BindingId) bound to the given name, including
/// those that were shadowed by later bindings. /// those that were shadowed by later bindings.
pub fn bindings_for_name(&self, name: &str) -> impl Iterator<Item = BindingId> + '_ { pub fn bindings_for_name(&self, name: &str) -> impl Iterator<Item = BindingId> + '_ {