mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-19 01:50:38 +00:00
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:
parent
d99b3bf661
commit
ecf61d49fa
13 changed files with 429 additions and 25 deletions
|
@ -3852,6 +3852,9 @@ where
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the existing binding, if any.
|
||||||
|
let existing_id = self.semantic.lookup(name);
|
||||||
|
|
||||||
// Add the bound exception name to the scope.
|
// Add the bound exception name to the scope.
|
||||||
let binding_id = self.add_binding(
|
let binding_id = self.add_binding(
|
||||||
name,
|
name,
|
||||||
|
@ -3862,14 +3865,6 @@ where
|
||||||
|
|
||||||
walk_except_handler(self, except_handler);
|
walk_except_handler(self, except_handler);
|
||||||
|
|
||||||
// Remove it from the scope immediately after.
|
|
||||||
self.add_binding(
|
|
||||||
name,
|
|
||||||
range,
|
|
||||||
BindingKind::UnboundException,
|
|
||||||
BindingFlags::empty(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the exception name wasn't used in the scope, emit a diagnostic.
|
// If the exception name wasn't used in the scope, emit a diagnostic.
|
||||||
if !self.semantic.is_used(binding_id) {
|
if !self.semantic.is_used(binding_id) {
|
||||||
if self.enabled(Rule::UnusedVariable) {
|
if self.enabled(Rule::UnusedVariable) {
|
||||||
|
@ -3889,6 +3884,13 @@ where
|
||||||
self.diagnostics.push(diagnostic);
|
self.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.add_binding(
|
||||||
|
name,
|
||||||
|
range,
|
||||||
|
BindingKind::UnboundException(existing_id),
|
||||||
|
BindingFlags::empty(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
None => walk_except_handler(self, except_handler),
|
None => walk_except_handler(self, except_handler),
|
||||||
}
|
}
|
||||||
|
@ -4236,7 +4238,7 @@ impl<'a> Checker<'a> {
|
||||||
let shadowed = &self.semantic.bindings[shadowed_id];
|
let shadowed = &self.semantic.bindings[shadowed_id];
|
||||||
if !matches!(
|
if !matches!(
|
||||||
shadowed.kind,
|
shadowed.kind,
|
||||||
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException,
|
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException(_),
|
||||||
) {
|
) {
|
||||||
let references = shadowed.references.clone();
|
let references = shadowed.references.clone();
|
||||||
let is_global = shadowed.is_global();
|
let is_global = shadowed.is_global();
|
||||||
|
|
|
@ -251,7 +251,7 @@ impl Renamer {
|
||||||
| BindingKind::ClassDefinition
|
| BindingKind::ClassDefinition
|
||||||
| BindingKind::FunctionDefinition
|
| BindingKind::FunctionDefinition
|
||||||
| BindingKind::Deletion
|
| BindingKind::Deletion
|
||||||
| BindingKind::UnboundException => {
|
| BindingKind::UnboundException(_) => {
|
||||||
Some(Edit::range_replacement(target.to_string(), binding.range))
|
Some(Edit::range_replacement(target.to_string(), binding.range))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -353,9 +353,59 @@ mod tests {
|
||||||
except Exception as x:
|
except Exception as x:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# No error here, though it should arguably be an F821 error. `x` will
|
||||||
|
# be unbound after the `except` block (assuming an exception is raised
|
||||||
|
# and caught).
|
||||||
print(x)
|
print(x)
|
||||||
"#,
|
"#,
|
||||||
"print_after_shadowing_except"
|
"print_in_body_after_shadowing_except"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
r#"
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
1 / 0
|
||||||
|
except ValueError as x:
|
||||||
|
pass
|
||||||
|
except ImportError as x:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# No error here, though it should arguably be an F821 error. `x` will
|
||||||
|
# be unbound after the `except` block (assuming an exception is raised
|
||||||
|
# and caught).
|
||||||
|
print(x)
|
||||||
|
"#,
|
||||||
|
"print_in_body_after_double_shadowing_except"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
r#"
|
||||||
|
def f():
|
||||||
|
try:
|
||||||
|
x = 3
|
||||||
|
except ImportError as x:
|
||||||
|
print(x)
|
||||||
|
else:
|
||||||
|
print(x)
|
||||||
|
"#,
|
||||||
|
"print_in_try_else_after_shadowing_except"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
r#"
|
||||||
|
def f():
|
||||||
|
list = [1, 2, 3]
|
||||||
|
|
||||||
|
for e in list:
|
||||||
|
if e % 2 == 0:
|
||||||
|
try:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
else:
|
||||||
|
print(e)
|
||||||
|
"#,
|
||||||
|
"print_in_if_else_after_shadowing_except"
|
||||||
)]
|
)]
|
||||||
#[test_case(
|
#[test_case(
|
||||||
r#"
|
r#"
|
||||||
|
@ -366,6 +416,79 @@ mod tests {
|
||||||
"#,
|
"#,
|
||||||
"double_del"
|
"double_del"
|
||||||
)]
|
)]
|
||||||
|
#[test_case(
|
||||||
|
r#"
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def f():
|
||||||
|
try:
|
||||||
|
pass
|
||||||
|
except ValueError as x:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This should resolve to the `x` in `x = 1`.
|
||||||
|
print(x)
|
||||||
|
"#,
|
||||||
|
"load_after_unbind_from_module_scope"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
r#"
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def f():
|
||||||
|
try:
|
||||||
|
pass
|
||||||
|
except ValueError as x:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
pass
|
||||||
|
except ValueError as x:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This should resolve to the `x` in `x = 1`.
|
||||||
|
print(x)
|
||||||
|
"#,
|
||||||
|
"load_after_multiple_unbinds_from_module_scope"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
r#"
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def f():
|
||||||
|
try:
|
||||||
|
pass
|
||||||
|
except ValueError as x:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def g():
|
||||||
|
try:
|
||||||
|
pass
|
||||||
|
except ValueError as x:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This should resolve to the `x` in `x = 1`.
|
||||||
|
print(x)
|
||||||
|
"#,
|
||||||
|
"load_after_unbind_from_nested_module_scope"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
r#"
|
||||||
|
class C:
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def f():
|
||||||
|
try:
|
||||||
|
pass
|
||||||
|
except ValueError as x:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This should raise an F821 error, rather than resolving to the
|
||||||
|
# `x` in `x = 1`.
|
||||||
|
print(x)
|
||||||
|
"#,
|
||||||
|
"load_after_unbind_from_class_scope"
|
||||||
|
)]
|
||||||
fn contents(contents: &str, snapshot: &str) {
|
fn contents(contents: &str, snapshot: &str) {
|
||||||
let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes));
|
let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes));
|
||||||
assert_messages!(snapshot, diagnostics);
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
---
|
||||||
|
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
5 | try:
|
||||||
|
6 | pass
|
||||||
|
7 | except ValueError as x:
|
||||||
|
| ^ F841
|
||||||
|
8 | pass
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
4 4 | def f():
|
||||||
|
5 5 | try:
|
||||||
|
6 6 | pass
|
||||||
|
7 |- except ValueError as x:
|
||||||
|
7 |+ except ValueError:
|
||||||
|
8 8 | pass
|
||||||
|
9 9 |
|
||||||
|
10 10 | try:
|
||||||
|
|
||||||
|
<filename>:12:26: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
10 | try:
|
||||||
|
11 | pass
|
||||||
|
12 | except ValueError as x:
|
||||||
|
| ^ F841
|
||||||
|
13 | pass
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
9 9 |
|
||||||
|
10 10 | try:
|
||||||
|
11 11 | pass
|
||||||
|
12 |- except ValueError as x:
|
||||||
|
12 |+ except ValueError:
|
||||||
|
13 13 | pass
|
||||||
|
14 14 |
|
||||||
|
15 15 | # This should resolve to the `x` in `x = 1`.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
---
|
||||||
|
<filename>:8:30: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
6 | try:
|
||||||
|
7 | pass
|
||||||
|
8 | except ValueError as x:
|
||||||
|
| ^ F841
|
||||||
|
9 | pass
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
5 5 | def f():
|
||||||
|
6 6 | try:
|
||||||
|
7 7 | pass
|
||||||
|
8 |- except ValueError as x:
|
||||||
|
8 |+ except ValueError:
|
||||||
|
9 9 | pass
|
||||||
|
10 10 |
|
||||||
|
11 11 | # This should raise an F821 error, rather than resolving to the
|
||||||
|
|
||||||
|
<filename>:13:15: F821 Undefined name `x`
|
||||||
|
|
|
||||||
|
11 | # This should raise an F821 error, rather than resolving to the
|
||||||
|
12 | # `x` in `x = 1`.
|
||||||
|
13 | print(x)
|
||||||
|
| ^ F821
|
||||||
|
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
---
|
||||||
|
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
5 | try:
|
||||||
|
6 | pass
|
||||||
|
7 | except ValueError as x:
|
||||||
|
| ^ F841
|
||||||
|
8 | pass
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
4 4 | def f():
|
||||||
|
5 5 | try:
|
||||||
|
6 6 | pass
|
||||||
|
7 |- except ValueError as x:
|
||||||
|
7 |+ except ValueError:
|
||||||
|
8 8 | pass
|
||||||
|
9 9 |
|
||||||
|
10 10 | # This should resolve to the `x` in `x = 1`.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
---
|
||||||
|
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
5 | try:
|
||||||
|
6 | pass
|
||||||
|
7 | except ValueError as x:
|
||||||
|
| ^ F841
|
||||||
|
8 | pass
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
4 4 | def f():
|
||||||
|
5 5 | try:
|
||||||
|
6 6 | pass
|
||||||
|
7 |- except ValueError as x:
|
||||||
|
7 |+ except ValueError:
|
||||||
|
8 8 | pass
|
||||||
|
9 9 |
|
||||||
|
10 10 | def g():
|
||||||
|
|
||||||
|
<filename>:13:30: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
11 | try:
|
||||||
|
12 | pass
|
||||||
|
13 | except ValueError as x:
|
||||||
|
| ^ F841
|
||||||
|
14 | pass
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
10 10 | def g():
|
||||||
|
11 11 | try:
|
||||||
|
12 12 | pass
|
||||||
|
13 |- except ValueError as x:
|
||||||
|
13 |+ except ValueError:
|
||||||
|
14 14 | pass
|
||||||
|
15 15 |
|
||||||
|
16 16 | # This should resolve to the `x` in `x = 1`.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
---
|
||||||
|
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
5 | try:
|
||||||
|
6 | 1 / 0
|
||||||
|
7 | except ValueError as x:
|
||||||
|
| ^ F841
|
||||||
|
8 | pass
|
||||||
|
9 | except ImportError as x:
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
4 4 |
|
||||||
|
5 5 | try:
|
||||||
|
6 6 | 1 / 0
|
||||||
|
7 |- except ValueError as x:
|
||||||
|
7 |+ except ValueError:
|
||||||
|
8 8 | pass
|
||||||
|
9 9 | except ImportError as x:
|
||||||
|
10 10 | pass
|
||||||
|
|
||||||
|
<filename>:9:27: F841 [*] Local variable `x` is assigned to but never used
|
||||||
|
|
|
||||||
|
7 | except ValueError as x:
|
||||||
|
8 | pass
|
||||||
|
9 | except ImportError as x:
|
||||||
|
| ^ F841
|
||||||
|
10 | pass
|
||||||
|
|
|
||||||
|
= help: Remove assignment to unused variable `x`
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
6 6 | 1 / 0
|
||||||
|
7 7 | except ValueError as x:
|
||||||
|
8 8 | pass
|
||||||
|
9 |- except ImportError as x:
|
||||||
|
9 |+ except ImportError:
|
||||||
|
10 10 | pass
|
||||||
|
11 11 |
|
||||||
|
12 12 | # No error here, though it should arguably be an F821 error. `x` will
|
||||||
|
|
||||||
|
|
|
@ -19,14 +19,6 @@ source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
7 |+ except Exception:
|
7 |+ except Exception:
|
||||||
8 8 | pass
|
8 8 | pass
|
||||||
9 9 |
|
9 9 |
|
||||||
10 10 | print(x)
|
10 10 | # No error here, though it should arguably be an F821 error. `x` will
|
||||||
|
|
||||||
<filename>:10:11: F821 Undefined name `x`
|
|
||||||
|
|
|
||||||
8 | pass
|
|
||||||
9 |
|
|
||||||
10 | print(x)
|
|
||||||
| ^ F821
|
|
||||||
|
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
---
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
---
|
||||||
|
|
|
@ -75,7 +75,7 @@ impl<'a> Binding<'a> {
|
||||||
pub const fn is_unbound(&self) -> bool {
|
pub const fn is_unbound(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.kind,
|
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
|
/// After the `except` block, `x` is unbound, despite the lack
|
||||||
/// of an explicit `del` statement.
|
/// 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! {
|
bitflags! {
|
||||||
|
|
|
@ -327,14 +327,56 @@ impl<'a> SemanticModel<'a> {
|
||||||
// ```
|
// ```
|
||||||
//
|
//
|
||||||
// The `x` in `print(x)` should be treated as unresolved.
|
// 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)
|
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);
|
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.
|
/// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases.
|
||||||
fn resolve_submodule(
|
fn resolve_submodule(
|
||||||
&self,
|
&self,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue