Skip to content

Commit 7b75aee

Browse files
IDrokin117Igor Drokin
andauthored
[pyupgrade] Avoid reporting __future__ features as unnecessary when they are used (UP010) (#19769)
## Summary Resolves #19561 Fixes the [unnecessary-future-import (UP010)](https://docs.astral.sh/ruff/rules/unnecessary-future-import/) rule to correctly identify when imported __future__ modules are actually used in the code, preventing false positives. I assume there is no way to check usage in `analyze::statements`, because we don't have any usage bindings for imports. To determine unused imports, we have to fully scan the file to create bindings and then check usage, similar to [unused-import (F401)](https://docs.astral.sh/ruff/rules/unused-import/#unused-import-f401). So, `Rule::UnnecessaryFutureImport` was moved from the `analyze::statements` to the `analyze::deferred_scopes` stage. This caused the need to change the logic of future import handling to a bindings-based approach. Also, the diagnostic report was changed. Before ``` | 1 | from __future__ import nested_scopes, generators | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP010 ``` after ``` | 1 | from __future__ import nested_scopes, generators | ^^^^^^^^^^^^^ UP010 ``` I believe this is the correct way, because `generators` may be used, but `nested_scopes` is not. ### Special case I've found out about some specific case. ```python from __future__ import nested_scopes nested_scopes = 1 ``` Here we can treat `nested_scopes` as an unused import because the variable `nested_scopes` shadows it and we can safely remove the future import (my fix does it). But [F401](https://docs.astral.sh/ruff/rules/unused-import/#unused-import-f401) not triggered for such case ([sandbox](https://play.ruff.rs/296d9c7e-0f02-4659-b0c0-78cc21f3de76)) ``` from foo import print_function print_function = 1 ``` In my mind, `print_function` here is an unused import and should be deleted (my IDE highlight it). What do you think? ## Test Plan Added test cases and snapshots: - Split test file into separate _0 and _1 files for appropriate checks. - Added test cases to verify fixes when future module are used. --------- Co-authored-by: Igor Drokin <[email protected]>
1 parent d04dcd9 commit 7b75aee

File tree

8 files changed

+215
-83
lines changed

8 files changed

+215
-83
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import nested_scopes, generators
2+
from __future__ import with_statement, unicode_literals
3+
4+
from __future__ import absolute_import, division
5+
from __future__ import generator_stop
6+
from __future__ import print_function, nested_scopes, generator_stop
7+
8+
print(with_statement)
9+
generators = 1
10+
11+
12+
class Foo():
13+
14+
def boo(self):
15+
print(division)
16+
17+
18+
__all__ = ["print_function", "generator_stop"]

crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
use ruff_python_ast::PythonVersion;
12
use ruff_python_semantic::{Binding, ScopeKind};
23

34
use crate::checkers::ast::Checker;
45
use crate::codes::Rule;
56
use crate::rules::{
67
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pep8_naming,
7-
pyflakes, pylint, ruff,
8+
pyflakes, pylint, pyupgrade, ruff,
89
};
910

1011
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
@@ -45,6 +46,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
4546
Rule::UnusedStaticMethodArgument,
4647
Rule::UnusedUnpackedVariable,
4748
Rule::UnusedVariable,
49+
Rule::UnnecessaryFutureImport,
4850
]) {
4951
return;
5052
}
@@ -224,6 +226,11 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
224226
if checker.is_rule_enabled(Rule::UnusedImport) {
225227
pyflakes::rules::unused_import(checker, scope);
226228
}
229+
if checker.is_rule_enabled(Rule::UnnecessaryFutureImport) {
230+
if checker.target_version() >= PythonVersion::PY37 {
231+
pyupgrade::rules::unnecessary_future_import(checker, scope);
232+
}
233+
}
227234

228235
if checker.is_rule_enabled(Rule::ImportPrivateName) {
229236
pylint::rules::import_private_name(checker, scope);

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -728,13 +728,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
728728
pylint::rules::non_ascii_module_import(checker, alias);
729729
}
730730
}
731-
if checker.is_rule_enabled(Rule::UnnecessaryFutureImport) {
732-
if checker.target_version() >= PythonVersion::PY37 {
733-
if let Some("__future__") = module {
734-
pyupgrade::rules::unnecessary_future_import(checker, stmt, names);
735-
}
736-
}
737-
}
738731
if checker.is_rule_enabled(Rule::DeprecatedMockImport) {
739732
pyupgrade::rules::deprecated_mock_import(checker, stmt);
740733
}

crates/ruff_linter/src/rules/pyupgrade/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ mod tests {
101101
#[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))]
102102
#[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.py"))]
103103
#[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))]
104-
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))]
104+
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_0.py"))]
105+
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010_1.py"))]
105106
#[test_case(Rule::UselessMetaclassType, Path::new("UP001.py"))]
106107
#[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))]
107108
#[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))]

crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
use std::collections::BTreeSet;
1+
use std::collections::{BTreeSet, HashMap};
22

3-
use itertools::Itertools;
3+
use itertools::{Itertools, chain};
4+
use ruff_python_semantic::NodeId;
45

56
use ruff_macros::{ViolationMetadata, derive_message_formats};
67
use ruff_python_ast::{self as ast, Alias, Stmt, StmtRef};
7-
use ruff_python_semantic::NameImport;
8+
use ruff_python_semantic::{NameImport, Scope};
89
use ruff_text_size::Ranged;
910

1011
use crate::checkers::ast::Checker;
@@ -111,68 +112,81 @@ pub(crate) fn is_import_required_by_isort(
111112
}
112113

113114
/// UP010
114-
pub(crate) fn unnecessary_future_import(checker: &Checker, stmt: &Stmt, names: &[Alias]) {
115-
let mut unused_imports: Vec<&Alias> = vec![];
116-
for alias in names {
117-
if alias.asname.is_some() {
118-
continue;
119-
}
120-
121-
if is_import_required_by_isort(
122-
&checker.settings().isort.required_imports,
123-
stmt.into(),
124-
alias,
125-
) {
126-
continue;
127-
}
128-
129-
if PY33_PLUS_REMOVE_FUTURES.contains(&alias.name.as_str())
130-
|| PY37_PLUS_REMOVE_FUTURES.contains(&alias.name.as_str())
131-
{
132-
unused_imports.push(alias);
115+
pub(crate) fn unnecessary_future_import(checker: &Checker, scope: &Scope) {
116+
let mut unused_imports: HashMap<NodeId, Vec<&Alias>> = HashMap::new();
117+
for future_name in chain(PY33_PLUS_REMOVE_FUTURES, PY37_PLUS_REMOVE_FUTURES).unique() {
118+
for binding_id in scope.get_all(future_name) {
119+
let binding = checker.semantic().binding(binding_id);
120+
if binding.kind.is_future_import() && binding.is_unused() {
121+
let Some(node_id) = binding.source else {
122+
continue;
123+
};
124+
125+
let stmt = checker.semantic().statement(node_id);
126+
if let Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) = stmt {
127+
let Some(alias) = names
128+
.iter()
129+
.find(|alias| alias.name.as_str() == binding.name(checker.source()))
130+
else {
131+
continue;
132+
};
133+
134+
if alias.asname.is_some() {
135+
continue;
136+
}
137+
138+
if is_import_required_by_isort(
139+
&checker.settings().isort.required_imports,
140+
stmt.into(),
141+
alias,
142+
) {
143+
continue;
144+
}
145+
unused_imports.entry(node_id).or_default().push(alias);
146+
}
147+
}
133148
}
134149
}
135150

136-
if unused_imports.is_empty() {
137-
return;
151+
for (node_id, unused_aliases) in unused_imports {
152+
let mut diagnostic = checker.report_diagnostic(
153+
UnnecessaryFutureImport {
154+
names: unused_aliases
155+
.iter()
156+
.map(|alias| alias.name.to_string())
157+
.sorted()
158+
.collect(),
159+
},
160+
checker.semantic().statement(node_id).range(),
161+
);
162+
163+
diagnostic.try_set_fix(|| {
164+
let statement = checker.semantic().statement(node_id);
165+
let parent = checker.semantic().parent_statement(node_id);
166+
let edit = fix::edits::remove_unused_imports(
167+
unused_aliases
168+
.iter()
169+
.map(|alias| &alias.name)
170+
.map(ast::Identifier::as_str),
171+
statement,
172+
parent,
173+
checker.locator(),
174+
checker.stylist(),
175+
checker.indexer(),
176+
)?;
177+
178+
let range = edit.range();
179+
let applicability = if checker.comment_ranges().intersects(range) {
180+
Applicability::Unsafe
181+
} else {
182+
Applicability::Safe
183+
};
184+
185+
Ok(
186+
Fix::applicable_edit(edit, applicability).isolate(Checker::isolation(
187+
checker.semantic().current_statement_parent_id(),
188+
)),
189+
)
190+
});
138191
}
139-
let mut diagnostic = checker.report_diagnostic(
140-
UnnecessaryFutureImport {
141-
names: unused_imports
142-
.iter()
143-
.map(|alias| alias.name.to_string())
144-
.sorted()
145-
.collect(),
146-
},
147-
stmt.range(),
148-
);
149-
150-
diagnostic.try_set_fix(|| {
151-
let statement = checker.semantic().current_statement();
152-
let parent = checker.semantic().current_statement_parent();
153-
let edit = fix::edits::remove_unused_imports(
154-
unused_imports
155-
.iter()
156-
.map(|alias| &alias.name)
157-
.map(ast::Identifier::as_str),
158-
statement,
159-
parent,
160-
checker.locator(),
161-
checker.stylist(),
162-
checker.indexer(),
163-
)?;
164-
165-
let range = edit.range();
166-
let applicability = if checker.comment_ranges().intersects(range) {
167-
Applicability::Unsafe
168-
} else {
169-
Applicability::Safe
170-
};
171-
172-
Ok(
173-
Fix::applicable_edit(edit, applicability).isolate(Checker::isolation(
174-
checker.semantic().current_statement_parent_id(),
175-
)),
176-
)
177-
});
178192
}
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
33
---
44
UP010 [*] Unnecessary `__future__` imports `generators`, `nested_scopes` for target Python version
5-
--> UP010.py:1:1
5+
--> UP010_0.py:1:1
66
|
77
1 | from __future__ import nested_scopes, generators
88
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -18,7 +18,7 @@ help: Remove unnecessary `__future__` import
1818
4 3 | from __future__ import generator_stop
1919

2020
UP010 [*] Unnecessary `__future__` imports `unicode_literals`, `with_statement` for target Python version
21-
--> UP010.py:2:1
21+
--> UP010_0.py:2:1
2222
|
2323
1 | from __future__ import nested_scopes, generators
2424
2 | from __future__ import with_statement, unicode_literals
@@ -36,7 +36,7 @@ help: Remove unnecessary `__future__` import
3636
5 4 | from __future__ import print_function, generator_stop
3737

3838
UP010 [*] Unnecessary `__future__` imports `absolute_import`, `division` for target Python version
39-
--> UP010.py:3:1
39+
--> UP010_0.py:3:1
4040
|
4141
1 | from __future__ import nested_scopes, generators
4242
2 | from __future__ import with_statement, unicode_literals
@@ -56,7 +56,7 @@ help: Remove unnecessary `__future__` import
5656
6 5 | from __future__ import invalid_module, generators
5757

5858
UP010 [*] Unnecessary `__future__` import `generator_stop` for target Python version
59-
--> UP010.py:4:1
59+
--> UP010_0.py:4:1
6060
|
6161
2 | from __future__ import with_statement, unicode_literals
6262
3 | from __future__ import absolute_import, division
@@ -77,7 +77,7 @@ help: Remove unnecessary `__future__` import
7777
7 6 |
7878

7979
UP010 [*] Unnecessary `__future__` imports `generator_stop`, `print_function` for target Python version
80-
--> UP010.py:5:1
80+
--> UP010_0.py:5:1
8181
|
8282
3 | from __future__ import absolute_import, division
8383
4 | from __future__ import generator_stop
@@ -97,7 +97,7 @@ help: Remove unnecessary `__future__` import
9797
8 7 | if True:
9898

9999
UP010 [*] Unnecessary `__future__` import `generators` for target Python version
100-
--> UP010.py:6:1
100+
--> UP010_0.py:6:1
101101
|
102102
4 | from __future__ import generator_stop
103103
5 | from __future__ import print_function, generator_stop
@@ -119,7 +119,7 @@ help: Remove unnecessary `__future__` import
119119
9 9 | from __future__ import generator_stop
120120

121121
UP010 [*] Unnecessary `__future__` import `generator_stop` for target Python version
122-
--> UP010.py:9:5
122+
--> UP010_0.py:9:5
123123
|
124124
8 | if True:
125125
9 | from __future__ import generator_stop
@@ -138,7 +138,7 @@ help: Remove unnecessary `__future__` import
138138
12 11 | if True:
139139

140140
UP010 [*] Unnecessary `__future__` import `generators` for target Python version
141-
--> UP010.py:10:5
141+
--> UP010_0.py:10:5
142142
|
143143
8 | if True:
144144
9 | from __future__ import generator_stop
@@ -159,7 +159,7 @@ help: Remove unnecessary `__future__` import
159159
13 12 | from __future__ import generator_stop
160160

161161
UP010 [*] Unnecessary `__future__` import `generator_stop` for target Python version
162-
--> UP010.py:13:5
162+
--> UP010_0.py:13:5
163163
|
164164
12 | if True:
165165
13 | from __future__ import generator_stop
@@ -178,7 +178,7 @@ help: Remove unnecessary `__future__` import
178178
15 14 | from __future__ import generators # comment
179179

180180
UP010 [*] Unnecessary `__future__` import `generators` for target Python version
181-
--> UP010.py:14:5
181+
--> UP010_0.py:14:5
182182
|
183183
12 | if True:
184184
13 | from __future__ import generator_stop
@@ -197,7 +197,7 @@ help: Remove unnecessary `__future__` import
197197
15 15 | from __future__ import generators # comment
198198

199199
UP010 [*] Unnecessary `__future__` import `generators` for target Python version
200-
--> UP010.py:15:5
200+
--> UP010_0.py:15:5
201201
|
202202
13 | from __future__ import generator_stop
203203
14 | from __future__ import invalid_module, generators

0 commit comments

Comments
 (0)