Restore existing bindings when unbinding caught exceptions (#5256)

## Summary

In the latest release, we made some improvements to the semantic model,
but our modifications to exception-unbinding are causing some
false-positives. For example:

```py
try:
    v = 3
except ImportError as v:
    print(v)
else:
    print(v)
```

In the latest release, we started unbinding `v` after the `except`
handler. (We used to restore the existing binding, the `v = 3`, but this
was quite complicated.) Because we don't have full branch analysis, we
can't then know that `v` is still bound in the `else` branch.

The solution here modifies `resolve_read` to skip-lookup when hitting
unbound exceptions. So when store the "unbind" for `except ImportError
as v`, we save the binding that it shadowed `v = 3`, and skip to that.

Closes #5249.

Closes #5250.
This commit is contained in:
Charlie Marsh 2023-06-21 12:53:58 -04:00 committed by GitHub
parent d99b3bf661
commit ecf61d49fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 429 additions and 25 deletions

View file

@ -75,7 +75,7 @@ impl<'a> Binding<'a> {
pub const fn is_unbound(&self) -> bool {
matches!(
self.kind,
BindingKind::Annotation | BindingKind::Deletion | BindingKind::UnboundException
BindingKind::Annotation | BindingKind::Deletion | BindingKind::UnboundException(_)
)
}
@ -427,7 +427,11 @@ pub enum BindingKind<'a> {
///
/// After the `except` block, `x` is unbound, despite the lack
/// of an explicit `del` statement.
UnboundException,
///
///
/// Stores the ID of the binding that was shadowed in the enclosing
/// scope, if any.
UnboundException(Option<BindingId>),
}
bitflags! {

View file

@ -327,14 +327,56 @@ impl<'a> SemanticModel<'a> {
// ```
//
// The `x` in `print(x)` should be treated as unresolved.
BindingKind::Deletion | BindingKind::UnboundException => {
//
// Similarly, given:
//
// ```python
// try:
// pass
// except ValueError as x:
// pass
//
// print(x)
//
// The `x` in `print(x)` should be treated as unresolved.
BindingKind::Deletion | BindingKind::UnboundException(None) => {
return ResolvedRead::UnboundLocal(binding_id)
}
// Otherwise, treat it as resolved.
_ => {
// If we hit an unbound exception that shadowed a bound name, resole to the
// bound name. For example, given:
//
// ```python
// x = 1
//
// try:
// pass
// except ValueError as x:
// pass
//
// print(x)
// ```
//
// The `x` in `print(x)` should resolve to the `x` in `x = 1`.
BindingKind::UnboundException(Some(binding_id)) => {
// Mark the binding as used.
let context = self.execution_context();
let reference_id = self.references.push(self.scope_id, range, context);
self.bindings[binding_id].references.push(reference_id);
// Mark any submodule aliases as used.
if let Some(binding_id) =
self.resolve_submodule(symbol, scope_id, binding_id)
{
let reference_id = self.references.push(self.scope_id, range, context);
self.bindings[binding_id].references.push(reference_id);
}
return ResolvedRead::Resolved(binding_id);
}
// Otherwise, treat it as resolved.
_ => return ResolvedRead::Resolved(binding_id),
}
}
@ -370,6 +412,50 @@ impl<'a> SemanticModel<'a> {
}
}
/// Lookup a symbol in the current scope. This is a carbon copy of [`Self::resolve_read`], but
/// doesn't add any read references to the resolved symbol.
pub fn lookup(&mut self, symbol: &str) -> Option<BindingId> {
if self.in_forward_reference() {
if let Some(binding_id) = self.scopes.global().get(symbol) {
if !self.bindings[binding_id].is_unbound() {
return Some(binding_id);
}
}
}
let mut seen_function = false;
for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() {
let scope = &self.scopes[scope_id];
if scope.kind.is_class() {
if seen_function && matches!(symbol, "__class__") {
return None;
}
if index > 0 {
continue;
}
}
if let Some(binding_id) = scope.get(symbol) {
match self.bindings[binding_id].kind {
BindingKind::Annotation => continue,
BindingKind::Deletion | BindingKind::UnboundException(None) => return None,
BindingKind::UnboundException(Some(binding_id)) => return Some(binding_id),
_ => return Some(binding_id),
}
}
if index == 0 && scope.kind.is_class() {
if matches!(symbol, "__module__" | "__qualname__") {
return None;
}
}
seen_function |= scope.kind.is_any_function();
}
None
}
/// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases.
fn resolve_submodule(
&self,