Skip to content

Commit f6cf9c5

Browse files
Fix import after use in function incorrectly removed (#178) (#321)
When a module-level import appears textually after a function that uses it, unimport incorrectly removed it. Function/async function bodies are deferred (only execute when called), so imports defined later at module scope are available at call time. Class bodies execute immediately, so the lineno check still applies for them. Added Name._is_deferred_usage() that walks from the Name's scope toward the Import's scope — if any scope on the path is a FunctionDef or AsyncFunctionDef, the usage is deferred and the lineno check is relaxed. Edge case tests cover: async functions, nested functions, class bodies (not deferred), methods inside classes, from-imports, and module-level sequential usage. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d313da1 commit f6cf9c5

22 files changed

+175
-2
lines changed

src/unimport/statement.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,26 @@ class Name:
140140
def is_attribute(self):
141141
return "." in self.name
142142

143+
def _is_deferred_usage(self, imp: Import | ImportFrom) -> bool:
144+
imp_scope = imp.scope
145+
scope = self.scope
146+
while scope is not None and scope != imp_scope:
147+
if isinstance(scope.node, (ast.FunctionDef, ast.AsyncFunctionDef)):
148+
return True
149+
scope = scope.parent
150+
return False
151+
143152
def match_2(self, imp: Import | ImportFrom) -> bool:
144153
if self.is_all:
145154
is_match = self.name == imp.name
146155
elif self.is_attribute:
147-
is_match = imp.lineno < self.lineno and (
156+
is_match = (imp.lineno < self.lineno or self._is_deferred_usage(imp)) and (
148157
".".join(self.name.split(".")[: len(imp)]) == imp.name or imp.is_match_sub_packages(self.name)
149158
)
150159
else:
151-
is_match = (imp.lineno < self.lineno) and (self.name == imp.name or imp.is_match_sub_packages(self.name))
160+
is_match = (imp.lineno < self.lineno or self._is_deferred_usage(imp)) and (
161+
self.name == imp.name or imp.is_match_sub_packages(self.name)
162+
)
152163

153164
return is_match
154165

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=2, name="path.join", is_all=False),
10+
]
11+
IMPORTS: list[Union[Import, ImportFrom]] = [
12+
ImportFrom(lineno=4, column=1, name="path", package="os", star=False, suggestions=[]),
13+
]
14+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=1, name="print", is_all=False),
10+
Name(lineno=1, name="sys.path", is_all=False),
11+
]
12+
IMPORTS: list[Union[Import, ImportFrom]] = [
13+
Import(lineno=3, column=1, name="sys", package="sys"),
14+
]
15+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = [
16+
Import(lineno=3, column=1, name="sys", package="sys"),
17+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=2, name="print", is_all=False),
10+
Name(lineno=2, name="sys.path", is_all=False),
11+
]
12+
IMPORTS: list[Union[Import, ImportFrom]] = [
13+
Import(lineno=4, column=1, name="sys", package="sys"),
14+
]
15+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=2, name="x", is_all=False),
10+
Name(lineno=2, name="sys.platform", is_all=False),
11+
]
12+
IMPORTS: list[Union[Import, ImportFrom]] = [
13+
Import(lineno=4, column=1, name="sys", package="sys"),
14+
]
15+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = [
16+
Import(lineno=4, column=1, name="sys", package="sys"),
17+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=2, name="print", is_all=False),
10+
Name(lineno=2, name="sys.path", is_all=False),
11+
]
12+
IMPORTS: list[Union[Import, ImportFrom]] = [
13+
Import(lineno=4, column=1, name="sys", package="sys"),
14+
]
15+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=3, name="os.getcwd", is_all=False),
10+
]
11+
IMPORTS: list[Union[Import, ImportFrom]] = [
12+
Import(lineno=5, column=1, name="os", package="os"),
13+
]
14+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from typing import Union
2+
3+
from unimport.statement import Import, ImportFrom, Name
4+
5+
__all__ = ["NAMES", "IMPORTS", "UNUSED_IMPORTS"]
6+
7+
8+
NAMES: list[Name] = [
9+
Name(lineno=3, name="print", is_all=False),
10+
Name(lineno=3, name="os.path.join", is_all=False),
11+
Name(lineno=3, name="os.path", is_all=False),
12+
]
13+
IMPORTS: list[Union[Import, ImportFrom]] = [
14+
Import(lineno=5, column=1, name="os", package="os"),
15+
]
16+
UNUSED_IMPORTS: list[Union[Import, ImportFrom]] = []
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def foo():
2+
return path.join("a", "b")
3+
4+
from os import path
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print(sys.path)

0 commit comments

Comments
 (0)