From 1d7cb22d42de90bba34019a80724f2373c1972d0 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 16 May 2026 20:28:52 -0400 Subject: [PATCH 01/20] fix: move revenueholdings-license to optional [license] extra with graceful import fallback - Remove revenueholdings-license>=0.1.0 from required dependencies - Add license optional-dependencies group with revenueholdings-license - Make require_license() import conditional: try/except ImportError - On ImportError, define no-op require_license() and print warning - Fixes CI breakage caused by revenueholdings-license not on PyPI (COM-79, COM-82, COM-83) --- pyproject.toml | 2 +- src/deadcode/cli.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5284403..2d378e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ classifiers = [ dependencies = [ "click>=8.1.0", "rich>=13.0.0", - "revenueholdings-license>=0.1.0", "pathspec>=0.11.0", "pyyaml>=6.0", ] @@ -36,6 +35,7 @@ dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", ] +license = ["revenueholdings-license>=0.1.0"] [project.scripts] deadcode = "deadcode.cli:cli" diff --git a/src/deadcode/cli.py b/src/deadcode/cli.py index 5b19b4b..5d04733 100644 --- a/src/deadcode/cli.py +++ b/src/deadcode/cli.py @@ -10,7 +10,13 @@ import click from rich.console import Console from rich.table import Table -from revenueholdings_license import require_license +try: + from revenueholdings_license import require_license +except ImportError: + import warnings + warnings.warn("revenueholdings-license not installed; license checks skipped", stacklevel=2) + def require_license(product: str) -> None: # type: ignore[misc] + pass from . import __version__ from .config import DeadCodeConfig From 56e8d908299bc5833a43e367aeeae5847a2ebad2 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 23 Jun 2026 02:51:37 -0400 Subject: [PATCH 02/20] docs: add AGENTS.md for agent discoverability --- AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2020121 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# deadcode + +## Purpose +Detect and remove unused exports, dead routes, orphaned CSS, and unreferenced components in TypeScript/React/Next.js projects. + +## Build & Test Commands +- Install: `pip install -e .` or `pip install deadcode-cli` +- Test: `pytest tests/` (or `python -m pytest tests/ -v --tb=short`) +- Lint: `ruff check .` +- Build: `pip install build twine && python -m build && twine check dist/*` +- CLI check: `deadcode --help` + +## Architecture +Key directories: +- `src/deadcode/` — Main package (CLI, analyzers for TS/JS/CSS/React/Next.js) +- `tests/` — Test suite +- `.github/workflows/` — CI/CD (ci.yml, pages.yml, publish.yml) +- `dist/` — Built distributions + +## Conventions +- Language: Python 3.10+ (with Node.js for TS analysis) +- Test framework: pytest +- CI: GitHub Actions (ci.yml, pages.yml, publish.yml) +- Linting: ruff +- Build system: setuptools +- Package layout: src/ layout +- Dependencies: click, rich, tree-sitter, tree-sitter-typescript, esprima +- CLI entry point: deadcode.cli:cli +- Default branch: main \ No newline at end of file From 4a906cc9115ef5d6f8804cba9f2b31569c5d6eed Mon Sep 17 00:00:00 2001 From: Jaixii Date: Sun, 28 Jun 2026 22:00:38 -0400 Subject: [PATCH 03/20] chore: add CODEOWNERS file --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..67b22f4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Coding-Dev-Tools From a3305d9b9d5f8b7a7924b0eb271973ff16059aa5 Mon Sep 17 00:00:00 2001 From: DevForge Engineer Date: Wed, 8 Jul 2026 01:36:11 -0400 Subject: [PATCH 04/20] cowork-bot: standardize copyright holder to 2025 Coding-Dev-Tools (was Revenue Holdings / stale 2026 year); W-directed fleet-wide pass --- LICENSE | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 2e0fcb2..3c4d9d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright 2026 Revenue Holdings +Copyright (c) 2025 Coding-Dev-Tools Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/package.json b/package.json index 51355b9..f3a7765 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "deadcode-cli", "version": "0.1.1", "description": "Find unused/dead code in Python projects. Static analysis tool to identify orphaned functions, classes, and imports.", - "author": "Revenue Holdings ", + "author": "Coding-Dev-Tools ", "license": "MIT", "repository": { "type": "git", diff --git a/pyproject.toml b/pyproject.toml index 47a1914..d3cd655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "CLI tool to detect and auto-remove unused exports, dead routes, o readme = "README.md" requires-python = ">=3.10" license = "MIT" -authors = [{name = "Revenue Holdings"}] +authors = [{name = "Coding-Dev-Tools"}] keywords = ["dead-code", "unused-exports", "typescript", "react", "nextjs", "cli", "css", "tree-shaking"] classifiers = [ "Development Status :: 4 - Beta", From f8809cdf3f65a62d69290efbb80350ea94fe26a4 Mon Sep 17 00:00:00 2001 From: cowork-bot Date: Thu, 16 Jul 2026 23:14:40 -0400 Subject: [PATCH 05/20] cowork-bot: fix false-positive orphaned CSS for CSS-module class accessors Detect and treat `styles.card` / `styles['card-hover']` (the canonical Next.js/React CSS-module consumption pattern) as used classes. Previously every *.module.css class was reported as orphaned and marked removable=True, which could delete live styles. Bracket accessors are recognized and non-module object access (util.foo) is not over-matched. Adds TestCSSModuleUsage regression tests; all 116 tests pass, ruff clean. Co-Authored-By: cowork-bot --- CHANGELOG.md | 6 ++ src/deadcode/scanner.py | 62 ++++++++++++ tests/test_scanner.py | 203 +++++++++++++++++----------------------- 3 files changed, 154 insertions(+), 117 deletions(-) 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/src/deadcode/scanner.py b/src/deadcode/scanner.py index c1a5e1c..a705891 100644 --- a/src/deadcode/scanner.py +++ b/src/deadcode/scanner.py @@ -132,6 +132,27 @@ def unreferenced_components(self) -> list[Finding]: r'"([^"]+)"\s*:\s*\["([^"]+)"\]', ) +# CSS-module accessor usage, two forms:: +# - dot: `styles.card` -> capture group 1 empty, full = "styles.card" +# - bracket: `styles['card-hover']` / `styles["card"]` -> capture group 1 = name +# We deliberately match either form after a binding identifier so both +# `styles.card` and `styles['card-hover']` register the class as used. +_CSS_MODULE_USAGE_PATTERN = re.compile( + r"""\b\w+\.(?:_?[\w$]+)|(\w+)\[['"]([\w$-]+)['"]\]""", +) + +# DOM classList API: `el.classList.add('foo')` / `toggle('bar')` / `remove(...)` +# / `contains('baz')`. Dynamic class names referenced here are real uses. +_CLASSLIST_PATTERN = re.compile( + r"""classList\.(?:add|remove|toggle|contains)\(\s*['"]([\w$-]+)['"]""", +) + +# Detect a CSS-module import so we know which source-file class accessors +# correspond to module classes (``import styles from './x.module.css'``). +_CSS_MODULE_IMPORT_PATTERN = re.compile( + r"""import\s+(?:type\s+)?(\w+)\s+from\s+['"]([^'"]*\.module\.css)['"]""", +) + # ── Scanner ─────────────────────────────────────────────────────────── @@ -218,6 +239,11 @@ def scan(self) -> ScanResult: # Parse className usage in TSX/JSX files if rel_path.endswith((".tsx", ".jsx")): self._parse_classname_usage(content, used_css_classes) + # CSS-module accessor usage (styles.card / styles['card']). + # Only register accessors imported as a CSS module so we don't + # treat every object property access (e.g. `user.name`) as a + # CSS class. + self._parse_css_module_usage(content, used_css_classes) # Parse components if rel_path.endswith((".tsx", ".jsx")): @@ -450,6 +476,42 @@ 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): + groups = m.groups() + # Bracket form `styles['card-hover']` -> groups = (binding, name). + # Dot form `styles.card` -> groups = (None, None); the whole match + # is "binding.attr". Distinguish by whether a bracket name exists. + bracket_binding, bracket_name = groups + if bracket_name is not None: + if bracket_binding in module_bindings: + used_css_classes.add(bracket_name) + continue + # Dot form `styles.card` -> the whole match is "binding.attr". + full = m.group(0) + binding, _, attr = full.partition(".") + if binding in module_bindings and attr and attr[0] != "_": + # Skip internal/private-ish accessors (e.g. styles.toString). + used_css_classes.add(attr) + 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_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