Mark `__all__` members as used at end-of-scope (#2733)

This commit is contained in:
Charlie Marsh 2023-02-10 14:32:05 -05:00 committed by GitHub
parent 3d650f9dd6
commit 0377834f9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 58 deletions

View File

@ -0,0 +1,4 @@
"""Test: late-binding of `__all__`."""
__all__ = ("bar",)
from foo import bar, baz

View File

@ -4326,11 +4326,6 @@ impl<'a> Checker<'a> {
_ => false, _ => false,
} { } {
let (all_names, all_names_flags) = extract_all_names(self, parent, current); let (all_names, all_names_flags) = extract_all_names(self, parent, current);
let all_bindings: Vec<usize> = all_names
.iter()
.filter_map(|name| current.bindings.get(name.as_str()))
.copied()
.collect();
if self.settings.rules.enabled(&Rule::InvalidAllFormat) { if self.settings.rules.enabled(&Rule::InvalidAllFormat) {
if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) { if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) {
@ -4346,15 +4341,6 @@ impl<'a> Checker<'a> {
} }
} }
// Mark all exported names as used-at-runtime.
for index in all_bindings {
self.bindings[index].mark_used(
GLOBAL_SCOPE_INDEX,
Range::from_located(expr),
ExecutionContext::Runtime,
);
}
self.add_binding( self.add_binding(
id, id,
Binding { Binding {
@ -4617,6 +4603,47 @@ impl<'a> Checker<'a> {
return; return;
} }
// Mark anything referenced in `__all__` as used.
let global_scope = &self.scopes[GLOBAL_SCOPE_INDEX];
let all_names: Option<(&Vec<String>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some((names, binding.range)),
_ => None,
});
let all_bindings: Option<(Vec<usize>, Range)> = all_names.map(|(names, range)| {
(
names
.iter()
.filter_map(|name| global_scope.bindings.get(name.as_str()).copied())
.collect(),
range,
)
});
if let Some((bindings, range)) = all_bindings {
for index in bindings {
self.bindings[index].mark_used(
GLOBAL_SCOPE_INDEX,
range,
ExecutionContext::Runtime,
);
}
}
// Extract `__all__` names from the global scope.
let all_names: Option<(Vec<&str>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => {
Some((names.iter().map(String::as_str).collect(), binding.range))
}
_ => None,
});
// Identify any valid runtime imports. If a module is imported at runtime, and // Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other // used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only. // imports from that model as typing-only.
@ -4687,29 +4714,17 @@ impl<'a> Checker<'a> {
continue; continue;
} }
let all_binding: Option<&Binding> = scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index]);
let all_names: Option<Vec<&str>> =
all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names.iter().map(String::as_str).collect()),
_ => None,
});
if self.settings.rules.enabled(&Rule::UndefinedExport) { if self.settings.rules.enabled(&Rule::UndefinedExport) {
if !scope.import_starred && !self.path.ends_with("__init__.py") { if !scope.import_starred && !self.path.ends_with("__init__.py") {
if let Some(all_binding) = all_binding { if let Some((names, range)) = &all_names {
if let Some(names) = &all_names { for &name in names {
for &name in names { if !scope.bindings.contains_key(name) {
if !scope.bindings.contains_key(name) { diagnostics.push(Diagnostic::new(
diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedExport {
pyflakes::rules::UndefinedExport { name: name.to_string(),
name: name.to_string(), },
}, *range,
all_binding.range, ));
));
}
} }
} }
} }
@ -4761,31 +4776,27 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::ImportStarUsage) { if self.settings.rules.enabled(&Rule::ImportStarUsage) {
if scope.import_starred { if scope.import_starred {
if let Some(all_binding) = all_binding { if let Some((names, range)) = &all_names {
if let Some(names) = &all_names { let mut from_list = vec![];
let mut from_list = vec![]; for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
for binding in if let BindingKind::StarImportation(level, module) = &binding.kind {
scope.bindings.values().map(|index| &self.bindings[*index]) from_list.push(helpers::format_import_from(
{ level.as_ref(),
if let BindingKind::StarImportation(level, module) = &binding.kind { module.as_deref(),
from_list.push(helpers::format_import_from( ));
level.as_ref(),
module.as_deref(),
));
}
} }
from_list.sort(); }
from_list.sort();
for &name in names { for &name in names {
if !scope.bindings.contains_key(name) { if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new( diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage { pyflakes::rules::ImportStarUsage {
name: name.to_string(), name: name.to_string(),
sources: from_list.clone(), sources: from_list.clone(),
}, },
all_binding.range, *range,
)); ));
}
} }
} }
} }

View File

@ -30,6 +30,7 @@ mod tests {
#[test_case(Rule::UnusedImport, Path::new("F401_6.py"); "F401_6")] #[test_case(Rule::UnusedImport, Path::new("F401_6.py"); "F401_6")]
#[test_case(Rule::UnusedImport, Path::new("F401_7.py"); "F401_7")] #[test_case(Rule::UnusedImport, Path::new("F401_7.py"); "F401_7")]
#[test_case(Rule::UnusedImport, Path::new("F401_8.py"); "F401_8")] #[test_case(Rule::UnusedImport, Path::new("F401_8.py"); "F401_8")]
#[test_case(Rule::UnusedImport, Path::new("F401_9.py"); "F401_9")]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"); "F402")] #[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"); "F402")]
#[test_case(Rule::ImportStar, Path::new("F403.py"); "F403")] #[test_case(Rule::ImportStar, Path::new("F403.py"); "F403")]
#[test_case(Rule::LateFutureImport, Path::new("F404.py"); "F404")] #[test_case(Rule::LateFutureImport, Path::new("F404.py"); "F404")]

View File

@ -0,0 +1,26 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
expression: diagnostics
---
- kind:
UnusedImport:
name: foo.baz
ignore_init: false
multiple: false
location:
row: 4
column: 21
end_location:
row: 4
column: 24
fix:
content:
- from foo import bar
location:
row: 4
column: 0
end_location:
row: 4
column: 24
parent: ~