diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 042ebc6..50994b2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ - uses: actions/checkout@v4
with:
persist-credentials: false
@@ -47,7 +47,7 @@ jobs:
contents: read
steps:
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ - uses: actions/checkout@v4
with:
persist-credentials: false
diff --git a/.github/workflows/cowork-auto-pr.yml b/.github/workflows/cowork-auto-pr.yml
index 91690e6..b27f04e 100644
--- a/.github/workflows/cowork-auto-pr.yml
+++ b/.github/workflows/cowork-auto-pr.yml
@@ -12,6 +12,14 @@ jobs:
ensure-pr:
runs-on: ubuntu-latest
steps:
+ # gh pr create requires a local git checkout to diff head against base;
+ # without this step every run failed with "not a git repository" and no
+ # PR was ever opened (fleet-wide defect: 11/11 seeded copies lacked it).
+ - name: Check out the pushed branch
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.ref_name }}
+ fetch-depth: 0
- name: Open PR for this branch if none exists
env:
GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index e64318a..31993c2 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -18,7 +18,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Pages
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 87d5a2a..e098b68 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -13,7 +13,7 @@ jobs:
id-token: write
steps:
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ - uses: actions/checkout@v4
with:
persist-credentials: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c44dabd..dd26d53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
'./mod'`, `export { type X } from './mod'`, and `export * from './mod'`) now mark
the forwarded symbols as used, preventing false-positive `removable` findings that
could delete live public API.
+- CSS-module classes consumed via the object-accessor pattern
+ (`import styles from './x.module.css';
`) are now
+ treated as used. Previously every `*.module.css` class was falsely reported as
+ orphaned CSS and marked `removable=True`, risking deletion of live styles. Bracket
+ accessors (`styles['card-hover']`) are also recognized; non-module object access
+ (e.g. `util.foo`) is not over-matched.
### Changed
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..6ac8caf
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,19 @@
+{
+ "name": "deadcode-cli",
+ "version": "0.1.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "deadcode-cli",
+ "version": "0.1.1",
+ "license": "MIT",
+ "bin": {
+ "deadcode": "cli.js"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 560949a..4492ddd 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.1.1",
"description": "Find unused/dead code in TypeScript, React, and Next.js projects. Scans for orphaned exports, dead routes, unreferenced components, and unused CSS module classes.",
"author": "Coding-Dev-Tools",
- "license"
+ "license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Coding-Dev-Tools/deadcode.git"
diff --git a/src/deadcode/scanner.py b/src/deadcode/scanner.py
index c1a5e1c..47f8cf3 100644
--- a/src/deadcode/scanner.py
+++ b/src/deadcode/scanner.py
@@ -132,6 +132,18 @@ def unreferenced_components(self) -> list[Finding]:
r'"([^"]+)"\s*:\s*\["([^"]+)"\]',
)
+# CSS-module dot and literal bracket access, including valid JavaScript
+# identifiers containing '$', and CSS class names beginning with '_'.
+_CSS_MODULE_USAGE_PATTERN = re.compile(
+ r"""(? ScanResult:
if rel_path.endswith((".tsx", ".jsx")):
self._parse_classname_usage(content, used_css_classes)
+ # CSS modules can also be consumed by plain TypeScript/JavaScript
+ # files, for example when exporting class-name maps to components.
+ if not self._is_css_file(rel_path):
+ self._parse_css_module_usage(content, used_css_classes)
+
# Parse components
if rel_path.endswith((".tsx", ".jsx")):
self._parse_components(content, rel_path, components)
@@ -281,6 +298,12 @@ def _collect_files(self) -> list[Path]:
continue
filepath = Path(root) / fname
+ # A source-file symlink may resolve outside the project. Such
+ # findings could otherwise make `remove` modify external files.
+ try:
+ filepath.resolve().relative_to(self.project_dir)
+ except (ValueError, OSError, RuntimeError):
+ continue
if self._is_scannable_file(rel_path):
files.append(filepath)
@@ -450,6 +473,30 @@ def _parse_classname_usage(self, content: str, used_css_classes: set[str]) -> No
for cls in group.split():
used_css_classes.add(cls)
+ def _parse_css_module_usage(self, content: str, used_css_classes: set[str]) -> None:
+ """Extract CSS-module class names consumed via object accessors.
+
+ The canonical Next.js/React pattern is::
+
+ import styles from './Card.module.css';
+
...
+
+ We first find any ``*.module.css`` imports (binding name ``styles``)
+ so that accessor use ``styles.card`` registers ``card`` as a used
+ class. Bracket access ``styles['card-hover']`` is also captured.
+ Without this, every CSS-module class is falsely reported as orphaned
+ and marked removable=True, risking deletion of live styles.
+ """
+ # Map the local binding (e.g. ``styles``) to a CSS-module import so
+ # only module accessors are treated as class names.
+ module_bindings = {m.group(1) for m in _CSS_MODULE_IMPORT_PATTERN.finditer(content)}
+ if not module_bindings:
+ return
+ for m in _CSS_MODULE_USAGE_PATTERN.finditer(content):
+ binding, dot_name, _, bracket_name = m.groups()
+ if binding in module_bindings:
+ used_css_classes.add(dot_name or bracket_name)
+
def _parse_components(self, content: str, rel_path: str, components: dict[str, str]) -> None:
"""Extract React component definitions."""
for m in _COMPONENT_PATTERN.finditer(content):
diff --git a/tests/test_css_module_boundaries.py b/tests/test_css_module_boundaries.py
new file mode 100644
index 0000000..7a874df
--- /dev/null
+++ b/tests/test_css_module_boundaries.py
@@ -0,0 +1,50 @@
+"""Protect live styles and the project boundary during a dead-code scan."""
+
+import pytest
+
+from deadcode.scanner import DeadCodeScanner
+
+
+@pytest.mark.parametrize("extension", ["ts", "js", "tsx", "jsx"])
+@pytest.mark.parametrize(
+ ("binding", "accessor", "class_name"),
+ [
+ ("styles", "._active", "_active"),
+ ("styles", "[ 'card-hover' ]", "card-hover"),
+ ("$styles", ".card", "card"),
+ ],
+)
+def test_css_module_accessors_preserve_live_classes(tmp_path, extension, binding, accessor, class_name):
+ (tmp_path / "Card.module.css").write_text(f".{class_name} {{ color: red; }}\n.unused {{}}\n")
+ (tmp_path / f"consumer.{extension}").write_text(
+ f"import {binding} from './Card.module.css';\nconst selected = {binding}{accessor};\n"
+ )
+
+ result = DeadCodeScanner(tmp_path).scan()
+
+ assert class_name not in {finding.name for finding in result.orphaned_css}
+ assert "unused" in {finding.name for finding in result.orphaned_css}
+
+
+def test_scanner_does_not_read_source_symlink_outside_project(tmp_path):
+ project = tmp_path / "project"
+ project.mkdir()
+ external = tmp_path / "external.ts"
+ external.write_text("export const externalLiveValue = 1;\n")
+ (project / "linked.ts").symlink_to(external)
+
+ result = DeadCodeScanner(project).scan()
+
+ assert result.files_scanned == 0
+ assert result.findings == []
+ assert external.read_text() == "export const externalLiveValue = 1;\n"
+
+
+def test_scanner_still_reads_internal_source_symlink(tmp_path):
+ target = tmp_path / "target.ts"
+ target.write_text("export const internalValue = 1;\n")
+ (tmp_path / "linked.ts").symlink_to(target)
+
+ result = DeadCodeScanner(tmp_path).scan()
+
+ assert result.files_scanned == 2
diff --git a/tests/test_scanner.py b/tests/test_scanner.py
index 0ddfbbb..77db3d6 100644
--- a/tests/test_scanner.py
+++ b/tests/test_scanner.py
@@ -43,33 +43,24 @@ def sample_project(tmp_path):
# src/components/UnusedWidget.tsx - never imported
widget = tmp_path / "src" / "components" / "UnusedWidget.tsx"
- widget.write_text(
- "export function UnusedWidget() {\n return
Unused
;\n}\n"
- )
+ widget.write_text("export function UnusedWidget() {\n return
Unused
;\n}\n")
# src/styles/main.css - with orphaned class
css = tmp_path / "src" / "styles" / "main.css"
css.parent.mkdir(parents=True, exist_ok=True)
- css.write_text(
- ".btn-primary {\n background: blue;\n}\n.orphaned-class {\n color: red;\n}\n"
- )
+ css.write_text(".btn-primary {\n background: blue;\n}\n.orphaned-class {\n color: red;\n}\n")
# src/app/page.tsx - Next.js page (entry point)
page = tmp_path / "src" / "app" / "page.tsx"
page.parent.mkdir(parents=True, exist_ok=True)
page.write_text(
- 'import { Button } from "../components/Button";\n'
- "export default function Page() {\n"
- " return
;\n"
- "}\n"
+ 'import { Button } from "../components/Button";\nexport default function Page() {\n return
;\n}\n'
)
# src/app/deadpage/page.tsx - dead route
deadpage = tmp_path / "src" / "app" / "deadpage" / "page.tsx"
deadpage.parent.mkdir(parents=True, exist_ok=True)
- deadpage.write_text(
- "export default function DeadPage() {\n return
Dead
;\n}\n"
- )
+ deadpage.write_text("export default function DeadPage() {\n return
Dead
;\n}\n")
return tmp_path
@@ -142,10 +133,7 @@ class TestExportParsing:
def test_named_exports(self, tmp_path):
f = tmp_path / "test.ts"
f.write_text(
- "export function foo() {}\n"
- "export const bar = 1;\n"
- "export type Baz = string;\n"
- "export interface Qux {}\n"
+ "export function foo() {}\nexport const bar = 1;\nexport type Baz = string;\nexport interface Qux {}\n"
)
scanner = DeadCodeScanner(tmp_path)
result = scanner.scan()
@@ -178,11 +166,10 @@ def test_used_exports_not_reported(self, tmp_path):
unused_names = {f.name for f in result.unused_exports}
assert "myFunc" not in unused_names
-
def test_type_import_counts_as_used(self, tmp_path):
"""import type { Foo } should mark Foo as used."""
mod = tmp_path / "mod.ts"
- mod.write_text('export type Foo = string;\n')
+ mod.write_text("export type Foo = string;\n")
app = tmp_path / "app.ts"
app.write_text('import type { Foo } from "./mod";\nconst x: Foo = "hello";\n')
@@ -195,7 +182,7 @@ def test_type_import_counts_as_used(self, tmp_path):
def test_mixed_default_and_named_import_counts_as_used(self, tmp_path):
"""Default + named should mark both as used."""
mod = tmp_path / "mod.ts"
- mod.write_text('export function myFunc() { return 1; }\n')
+ mod.write_text("export function myFunc() { return 1; }\n")
app = tmp_path / "app.ts"
app.write_text('import Default, { myFunc } from "./mod";\nmyFunc();\n')
@@ -209,7 +196,7 @@ def test_mixed_default_and_named_import_counts_as_used(self, tmp_path):
def test_type_only_import_marks_as_used(self, tmp_path):
"""`import { type Foo } from ...` should mark Foo as used."""
mod = tmp_path / "mod.ts"
- mod.write_text('export type Foo = string;\n')
+ mod.write_text("export type Foo = string;\n")
app = tmp_path / "app.ts"
app.write_text('import { type Foo } from "./mod";\nconst x: Foo = "hi";\n')
@@ -222,7 +209,7 @@ def test_type_only_import_marks_as_used(self, tmp_path):
def test_mixed_default_and_type_only_import_marks_as_used(self, tmp_path):
"""`import Default, { type Foo } from ...` should mark Foo as used."""
mod = tmp_path / "mod.ts"
- mod.write_text('export default function Default() { return 1; }\nexport type Foo = string;\n')
+ mod.write_text("export default function Default() { return 1; }\nexport type Foo = string;\n")
app = tmp_path / "app.ts"
app.write_text('import Default, { type Foo } from "./mod";\nconst x: Foo = "hi";\n')
@@ -234,19 +221,12 @@ def test_mixed_default_and_type_only_import_marks_as_used(self, tmp_path):
assert "Foo" not in unused_names
-
class TestCSSParsing:
def test_orphaned_css_detection(self, tmp_path):
css = tmp_path / "styles.css"
- css.write_text(
- ".used-class { color: blue; }\n.orphaned-class { color: red; }\n"
- )
+ css.write_text(".used-class { color: blue; }\n.orphaned-class { color: red; }\n")
component = tmp_path / "Component.tsx"
- component.write_text(
- "export function Component() {\n"
- ' return
Hi
;\n'
- "}\n"
- )
+ component.write_text('export function Component() {\n return
Hi
;\n}\n')
scanner = DeadCodeScanner(tmp_path)
result = scanner.scan()
@@ -256,13 +236,59 @@ def test_orphaned_css_detection(self, tmp_path):
assert "used-class" not in orphaned
+class TestCSSModuleUsage:
+ """CSS-module class accessors (``styles.card``) must count as uses.
+
+ Regression coverage for a bug where every ``*.module.css`` class was
+ falsely reported as orphaned (and removable=True), risking deletion of
+ live styles consumed via the object-accessor pattern.
+ """
+
+ def test_module_accessor_used_not_orphaned(self, tmp_path):
+ css = tmp_path / "Card.module.css"
+ css.write_text(".card { color: red; }\n.unusedClass { color: blue; }\n")
+ component = tmp_path / "Card.tsx"
+ component.write_text(
+ "import styles from './Card.module.css';\n"
+ "export function Card() { return
hi
; }\n"
+ )
+
+ result = DeadCodeScanner(tmp_path).scan()
+ orphaned = {f.name for f in result.orphaned_css}
+ assert "card" not in orphaned
+ assert "unusedClass" in orphaned
+
+ def test_module_bracket_accessor(self, tmp_path):
+ css = tmp_path / "M.module.css"
+ css.write_text(".card-hover { } .orphan { }\n")
+ component = tmp_path / "M.tsx"
+ component.write_text(
+ "import s from './M.module.css';\nexport const X = () =>
;\n"
+ )
+
+ result = DeadCodeScanner(tmp_path).scan()
+ orphaned = {f.name for f in result.orphaned_css}
+ assert "card-hover" not in orphaned
+ assert "orphan" in orphaned
+
+ def test_non_module_object_access_not_counted(self, tmp_path):
+ # `util.foo` where util is NOT a *.module.css import must not be
+ # treated as a CSS-module class use (guards against over-matching).
+ css = tmp_path / "M.module.css"
+ css.write_text(".secret { }\n")
+ component = tmp_path / "M.tsx"
+ component.write_text("import util from './util';\nexport const X = () =>
;\n")
+
+ result = DeadCodeScanner(tmp_path).scan()
+ orphaned = {f.name for f in result.orphaned_css}
+ assert "secret" in orphaned
+
+
class TestRouteDetection:
def test_nextjs_app_router_route(self, tmp_path):
page = tmp_path / "app" / "about" / "page.tsx"
page.parent.mkdir(parents=True, exist_ok=True)
- page.write_text(
- "export default function About() { return
About
; }\n"
- )
+ page.write_text("export default function About() { return
About
; }\n")
scanner = DeadCodeScanner(tmp_path)
result = scanner.scan()
@@ -285,14 +311,10 @@ def test_root_route_not_dead(self, tmp_path):
def test_linked_route_not_dead(self, tmp_path):
page = tmp_path / "app" / "page.tsx"
page.parent.mkdir(parents=True, exist_ok=True)
- page.write_text(
- 'export default function Home() { return
About; }\n'
- )
+ page.write_text('export default function Home() { return
About; }\n')
about = tmp_path / "app" / "about" / "page.tsx"
about.parent.mkdir(parents=True, exist_ok=True)
- about.write_text(
- "export default function About() { return
About
; }\n"
- )
+ about.write_text("export default function About() { return
About
; }\n")
scanner = DeadCodeScanner(tmp_path)
result = scanner.scan()
@@ -320,18 +342,14 @@ def test_scan_command(self, runner, sample_project):
assert "DeadCode Scan" in result.output
def test_scan_json_output(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "--json-output"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "--json-output"])
assert result.exit_code == 0
data = json.loads(result.output, strict=False)
assert "findings" in data
assert "files_scanned" in data
def test_scan_category_filter(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "-c", "orphaned_css"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "-c", "orphaned_css"])
assert result.exit_code == 0
# Text output should mention the category
assert "Orphaned CSS" in result.output
@@ -343,11 +361,7 @@ def test_scan_nonexistent_dir(self, runner):
def test_remove_dry_run(self, runner, sample_project):
result = runner.invoke(cli, ["-p", str(sample_project), "remove", "--dry-run"])
assert result.exit_code == 0
- assert (
- "WOULD REMOVE" in result.output
- or "Nothing removable" in result.output
- or result.exit_code == 0
- )
+ assert "WOULD REMOVE" in result.output or "Nothing removable" in result.output or result.exit_code == 0
def test_stats_command(self, runner, sample_project):
result = runner.invoke(cli, ["-p", str(sample_project), "stats"])
@@ -361,9 +375,7 @@ def test_scan_ignore_option(self, runner, tmp_path):
mod.parent.mkdir(parents=True, exist_ok=True)
mod.write_text("export function unusedFunc() { return 1; }\n")
- result = runner.invoke(
- cli, ["-p", str(tmp_path), "-i", "src/", "scan", "--json-output"]
- )
+ result = runner.invoke(cli, ["-p", str(tmp_path), "-i", "src/", "scan", "--json-output"])
assert result.exit_code == 0
import json
@@ -382,9 +394,7 @@ def test_remove_category_filter(self, runner, sample_project):
def test_remove_nonexistent_dir(self, runner):
"""remove should give graceful error for nonexistent project dir."""
- result = runner.invoke(
- cli, ["-p", "/nonexistent/test/path", "remove", "--dry-run"]
- )
+ result = runner.invoke(cli, ["-p", "/nonexistent/test/path", "remove", "--dry-run"])
assert result.exit_code != 0
assert "not found" in result.output
@@ -421,12 +431,7 @@ def test_multiline_export_list_detected(self, tmp_path):
mod = tmp_path / "src" / "mod.ts"
mod.parent.mkdir(parents=True, exist_ok=True)
mod.write_text(
- "function Alpha() { return 1; }\n"
- "function Beta() { return 2; }\n"
- "export {\n"
- " Alpha,\n"
- " Beta,\n"
- "}\n"
+ "function Alpha() { return 1; }\nfunction Beta() { return 2; }\nexport {\n Alpha,\n Beta,\n}\n"
)
scanner = DeadCodeScanner(tmp_path)
@@ -455,23 +460,14 @@ def test_multiline_export_list_used_not_reported(self, tmp_path):
export_names = {f.name for f in result.unused_exports}
# usedInApp appears in both an inline export and the export-list; it's imported so should be absent
- assert "usedInApp" not in export_names, (
- "usedInApp is imported — should not be reported"
- )
- assert "alsoUnused" in export_names, (
- "alsoUnused is never imported — should be reported"
- )
+ assert "usedInApp" not in export_names, "usedInApp is imported — should not be reported"
+ assert "alsoUnused" in export_names, "alsoUnused is never imported — should be reported"
def test_multiline_export_list_with_aliases(self, tmp_path):
"""export { Foo as Bar } aliases: the local name Foo should be tracked, not the alias."""
mod = tmp_path / "src" / "mod.ts"
mod.parent.mkdir(parents=True, exist_ok=True)
- mod.write_text(
- "function InternalName() { return 1; }\n"
- "export {\n"
- " InternalName as PublicName,\n"
- "}\n"
- )
+ mod.write_text("function InternalName() { return 1; }\nexport {\n InternalName as PublicName,\n}\n")
scanner = DeadCodeScanner(tmp_path)
result = scanner.scan()
@@ -569,43 +565,31 @@ class TestScanFormat:
"""Tests for the new --format scan option."""
def test_format_compact(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "--format=compact"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "--format=compact"])
assert result.exit_code == 0
def test_format_github(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "--format=github"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "--format=github"])
assert result.exit_code == 0
def test_format_compact_with_findings(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "--format=compact"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "--format=compact"])
assert result.exit_code == 0
assert "unusedHelper" in result.output or "No dead code" in result.output
def test_format_github_with_findings(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "--format=github"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "--format=github"])
assert result.exit_code == 0
assert "::" in result.output or "No dead code" in result.output
def test_format_json_legacy_alias(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "--json-output"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "--json-output"])
assert result.exit_code == 0
data = json.loads(result.output, strict=False)
assert "findings" in data
def test_format_json_explicit(self, runner, sample_project):
- result = runner.invoke(
- cli, ["-p", str(sample_project), "scan", "--format=json"]
- )
+ result = runner.invoke(cli, ["-p", str(sample_project), "scan", "--format=json"])
assert result.exit_code == 0
data = json.loads(result.output, strict=False)
assert "findings" in data
@@ -635,7 +619,7 @@ class TestReexportForwarding:
def test_named_reexport_marks_source_as_used(self, tmp_path):
src = tmp_path / "foo.ts"
- src.write_text('export function helper() { return 1; }\n')
+ src.write_text("export function helper() { return 1; }\n")
index = tmp_path / "index.ts"
index.write_text('export { helper } from "./foo";\n')
@@ -646,7 +630,7 @@ def test_named_reexport_marks_source_as_used(self, tmp_path):
def test_renamed_reexport_marks_source_as_used(self, tmp_path):
src = tmp_path / "foo.ts"
- src.write_text('export function helper() { return 1; }\n')
+ src.write_text("export function helper() { return 1; }\n")
index = tmp_path / "index.ts"
index.write_text('export { helper as primaryHelper } from "./foo";\n')
@@ -657,7 +641,7 @@ def test_renamed_reexport_marks_source_as_used(self, tmp_path):
def test_type_named_reexport_marks_source_as_used(self, tmp_path):
src = tmp_path / "types.ts"
- src.write_text('export type Foo = string;\n')
+ src.write_text("export type Foo = string;\n")
index = tmp_path / "index.ts"
index.write_text('export { type Foo } from "./types";\n')
@@ -668,17 +652,9 @@ def test_type_named_reexport_marks_source_as_used(self, tmp_path):
def test_multiline_named_reexport(self, tmp_path):
src = tmp_path / "foo.ts"
- src.write_text(
- 'export const alpha = 1;\n'
- 'export const beta = 2;\n'
- )
+ src.write_text("export const alpha = 1;\nexport const beta = 2;\n")
index = tmp_path / "index.ts"
- index.write_text(
- 'export {\n'
- ' alpha,\n'
- ' beta,\n'
- '} from "./foo";\n'
- )
+ index.write_text('export {\n alpha,\n beta,\n} from "./foo";\n')
result = DeadCodeScanner(tmp_path).scan()
@@ -688,10 +664,7 @@ def test_multiline_named_reexport(self, tmp_path):
def test_star_reexport_marks_all_source_exports_as_used(self, tmp_path):
src = tmp_path / "widgets.ts"
- src.write_text(
- 'export function widgetA() {}\n'
- 'export function widgetB() {}\n'
- )
+ src.write_text("export function widgetA() {}\nexport function widgetB() {}\n")
index = tmp_path / "index.ts"
index.write_text('export * from "./widgets";\n')
@@ -704,7 +677,7 @@ def test_star_reexport_marks_all_source_exports_as_used(self, tmp_path):
def test_star_reexport_from_directory_index(self, tmp_path):
mod = tmp_path / "widgets" / "index.ts"
mod.parent.mkdir(parents=True, exist_ok=True)
- mod.write_text('export function widgetA() {}\n')
+ mod.write_text("export function widgetA() {}\n")
index = tmp_path / "index.ts"
index.write_text('export * from "./widgets";\n')
@@ -717,11 +690,7 @@ def test_local_export_list_still_reported_when_unused(self, tmp_path):
# Regression guard: a *local* `export { ... }` (no `from`) must still be
# tracked and flagged when unused — the re-export fix must not suppress it.
f = tmp_path / "test.ts"
- f.write_text(
- 'const alpha = 1;\n'
- 'const beta = 2;\n'
- 'export { alpha, beta };\n'
- )
+ f.write_text("const alpha = 1;\nconst beta = 2;\nexport { alpha, beta };\n")
result = DeadCodeScanner(tmp_path).scan()
@@ -732,7 +701,7 @@ def test_local_export_list_still_reported_when_unused(self, tmp_path):
def test_reexport_from_package_does_not_crash_or_falsely_mark(self, tmp_path):
# Re-export from a bare package specifier must be ignored for resolution.
src = tmp_path / "foo.ts"
- src.write_text('export function localOnly() {}\n')
+ src.write_text("export function localOnly() {}\n")
index = tmp_path / "index.ts"
index.write_text('export { useState } from "react";\n')