Skip to content

Commit f41f880

Browse files
author
DevForge Engineer
committed
cowork-bot: fix import parsing for type-only and mixed default+named imports
1 parent e5fb25b commit f41f880

2 files changed

Lines changed: 45 additions & 10 deletions

File tree

src/deadcode/scanner.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ def unreferenced_components(self) -> list[Finding]:
8787
r"\.([a-zA-Z_][\w-]*(?::[\w-]+)*)\s*(?:\{|,|\[)",
8888
)
8989

90-
# import statements
90+
# import statements (handles default, named, type-only, and mixed forms)
9191
_IMPORT_PATTERN = re.compile(
92-
r"import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['\"]([^'\"]+)['\"]",
92+
r"import\s+(?:type\s+)?(?:(\w+))?\s*,?\s*(?:\{([^}]+)\})?\s*from\s+['\"]([^'\"]+)['\"]",
9393
)
9494

9595
# className="..." or className={...} in JSX
@@ -300,18 +300,26 @@ def _parse_exports(
300300
def _parse_imports(
301301
self, content: str, rel_path: str, imports: dict[str, set[str]]
302302
) -> None:
303-
"""Extract import names from a file."""
303+
"""Extract import names from a file.
304+
305+
Handles default, named, type-only, and mixed import forms:
306+
import Foo from '...'
307+
import { Foo, Bar } from '...'
308+
import type { Foo } from '...'
309+
import Foo, { Bar } from '...'
310+
"""
304311
for m in _IMPORT_PATTERN.finditer(content):
305-
# Named imports: import { Foo, Bar } from '...'
306-
if m.group(1):
307-
names = [n.strip().split(" as ")[0].strip() for n in m.group(1).split(",")]
312+
default_import = m.group(1)
313+
named_imports = m.group(2)
314+
module_path = m.group(3)
315+
316+
if default_import:
317+
imports.setdefault(default_import, set()).add(rel_path)
318+
if named_imports:
319+
names = [n.strip().split(" as ")[0].strip() for n in named_imports.split(",")]
308320
for name in names:
309321
if name:
310322
imports.setdefault(name, set()).add(rel_path)
311-
# Default import: import Foo from '...'
312-
elif m.group(2):
313-
name = m.group(2)
314-
imports.setdefault(name, set()).add(rel_path)
315323

316324
def _parse_css_classes(
317325
self, content: str, rel_path: str, css_classes: dict[str, list[tuple[str, int]]]

tests/test_scanner.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,33 @@ def test_used_exports_not_reported(self, tmp_path):
185185
unused_names = {f.name for f in result.unused_exports}
186186
assert "myFunc" not in unused_names
187187

188+
def test_type_import_counts_as_used(self, tmp_path):
189+
"""import type { Foo } should mark Foo as used."""
190+
mod = tmp_path / "mod.ts"
191+
mod.write_text('export type Foo = string;\n')
192+
app = tmp_path / "app.ts"
193+
app.write_text('import type { Foo } from "./mod";\nconst x: Foo = "hello";\n')
194+
195+
scanner = DeadCodeScanner(tmp_path)
196+
result = scanner.scan()
197+
198+
unused_names = {f.name for f in result.unused_exports}
199+
assert "Foo" not in unused_names
200+
201+
def test_mixed_default_and_named_import_counts_as_used(self, tmp_path):
202+
"""import默认 + named should mark both as used."""
203+
mod = tmp_path / "mod.ts"
204+
mod.write_text('export function myFunc() { return 1; }\n')
205+
app = tmp_path / "app.ts"
206+
app.write_text('import Default, { myFunc } from "./mod";\nmyFunc();\n')
207+
208+
scanner = DeadCodeScanner(tmp_path)
209+
result = scanner.scan()
210+
211+
unused_names = {f.name for f in result.unused_exports}
212+
assert "myFunc" not in unused_names
213+
assert "Default" not in unused_names
214+
188215

189216
class TestCSSParsing:
190217
def test_orphaned_css_detection(self, tmp_path):

0 commit comments

Comments
 (0)