ghostbuster-cli 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/CHANGELOG.md +12 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/PKG-INFO +6 -6
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/README.md +5 -5
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/pyproject.toml +1 -1
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/__init__.py +1 -1
- ghostbuster_cli-0.3.0/src/ghostbuster/core/dead_imports.py +416 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/orphan_files.py +3 -0
- ghostbuster_cli-0.3.0/src/ghostbuster/core/phantom_env.py +246 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/import_fixer.py +26 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_cli.py +1 -1
- ghostbuster_cli-0.3.0/tests/test_node_scanner.py +191 -0
- ghostbuster_cli-0.2.2/src/ghostbuster/core/dead_imports.py +0 -273
- ghostbuster_cli-0.2.2/src/ghostbuster/core/phantom_env.py +0 -196
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.gitignore +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/.pre-commit-hooks.yaml +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/CONTRIBUTING.md +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/LICENSE +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/assets/npm_package_icon.png +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/assets/readme_header.png +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/assets/terminal_icon.png +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/__main__.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/__init__.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/app.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/bust.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/display.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/cli/scan.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/config.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/__init__.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/models.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/scanner.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/scoring.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/core/zombie_code.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/__init__.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/env_fixer.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/src/ghostbuster/fixers/gitignore_fixer.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/__init__.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/conftest.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_dead_imports.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_fixers.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_orphan_files.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_phantom_env.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_scoring.py +0 -0
- {ghostbuster_cli-0.2.2 → ghostbuster_cli-0.3.0}/tests/test_zombie_code.py +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Node.js, JavaScript, and TypeScript Support**:
|
|
15
|
+
- **Dead Imports**: Scans `package.json` (`dependencies`, `devDependencies`, `peerDependencies`) and verifies against imports in `.js`, `.mjs`, `.cjs`, `.jsx`, `.ts`, `.mts`, `.cts`, `.tsx`, `.vue`, and `.svelte` files.
|
|
16
|
+
- **Scoped Packages & Subpaths**: Supports `@scope/pkg` and `pkg/subpath` imports.
|
|
17
|
+
- **Phantom Env**: Scans `process.env.VAR`, `process.env['VAR']`, `import.meta.env.VITE_VAR`, and `NEXT_PUBLIC_VAR`.
|
|
18
|
+
- **Auto-Fix for `package.json`**: `ghostbuster bust --confirm` automatically removes dead dependencies from `package.json` with preserved JSON formatting.
|
|
19
|
+
- **Orphan Files**: Added detection for Node log files (`npm-debug.log`, `yarn-error.log`, `pnpm-debug.log`).
|
|
20
|
+
- **Full-Stack Scanning**: Simultaneously scans Python backends and JS/TS frontends in monorepos with a unified Ghost Score.
|
|
21
|
+
|
|
10
22
|
## [0.2.2] - 2026-08-29
|
|
11
23
|
|
|
12
24
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ghostbuster-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Find and bust the ghosts haunting your codebase - unused deps, dead code, orphan files, and phantom env vars.
|
|
5
5
|
Project-URL: Homepage, https://github.com/adewanggar/ghostbuster-cli
|
|
6
6
|
Project-URL: Documentation, https://github.com/adewanggar/ghostbuster-cli#readme
|
|
@@ -225,11 +225,11 @@ Ghostbuster uses **pure AST analysis** - no runtime imports, no code execution,
|
|
|
225
225
|
|
|
226
226
|
## Roadmap
|
|
227
227
|
|
|
228
|
-
- <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / TypeScript support (
|
|
229
|
-
- Pre-commit hook integration
|
|
230
|
-
- GitHub Actions reporter (comment Ghost Score on PRs)
|
|
231
|
-
- Config inheritance for monorepos
|
|
232
|
-
- Ghost Score history tracking & trend chart
|
|
228
|
+
- [x] <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / JavaScript / TypeScript support (`package.json`, `process.env`, `import.meta.env`)
|
|
229
|
+
- [x] Pre-commit hook integration (`.pre-commit-hooks.yaml`)
|
|
230
|
+
- [ ] GitHub Actions reporter (comment Ghost Score on PRs)
|
|
231
|
+
- [ ] Config inheritance for monorepos
|
|
232
|
+
- [ ] Ghost Score history tracking & trend chart
|
|
233
233
|
|
|
234
234
|
## Contributing
|
|
235
235
|
|
|
@@ -187,11 +187,11 @@ Ghostbuster uses **pure AST analysis** - no runtime imports, no code execution,
|
|
|
187
187
|
|
|
188
188
|
## Roadmap
|
|
189
189
|
|
|
190
|
-
- <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / TypeScript support (
|
|
191
|
-
- Pre-commit hook integration
|
|
192
|
-
- GitHub Actions reporter (comment Ghost Score on PRs)
|
|
193
|
-
- Config inheritance for monorepos
|
|
194
|
-
- Ghost Score history tracking & trend chart
|
|
190
|
+
- [x] <img src="https://raw.githubusercontent.com/adewanggar/ghostbuster-cli/main/assets/npm_package_icon.png" width="18" height="18" valign="middle" /> Node.js / JavaScript / TypeScript support (`package.json`, `process.env`, `import.meta.env`)
|
|
191
|
+
- [x] Pre-commit hook integration (`.pre-commit-hooks.yaml`)
|
|
192
|
+
- [ ] GitHub Actions reporter (comment Ghost Score on PRs)
|
|
193
|
+
- [ ] Config inheritance for monorepos
|
|
194
|
+
- [ ] Ghost Score history tracking & trend chart
|
|
195
195
|
|
|
196
196
|
## Contributing
|
|
197
197
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ghostbuster-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Find and bust the ghosts haunting your codebase - unused deps, dead code, orphan files, and phantom env vars."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Dead Import Scanner - detects dependencies declared but never imported.
|
|
2
|
+
|
|
3
|
+
Checks requirements.txt, pyproject.toml, and package.json for declared
|
|
4
|
+
dependencies, then walks Python, JavaScript, and TypeScript files to see which
|
|
5
|
+
are actually imported.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ghostbuster.core.models import Ghost, GhostCategory, Severity
|
|
17
|
+
|
|
18
|
+
# Mapping from PyPI package names to their actual import names.
|
|
19
|
+
PACKAGE_TO_IMPORT: dict[str, str | list[str]] = {
|
|
20
|
+
"pillow": "PIL",
|
|
21
|
+
"scikit-learn": "sklearn",
|
|
22
|
+
"scikit-image": "skimage",
|
|
23
|
+
"opencv-python": "cv2",
|
|
24
|
+
"opencv-python-headless": "cv2",
|
|
25
|
+
"python-dateutil": "dateutil",
|
|
26
|
+
"pyyaml": "yaml",
|
|
27
|
+
"beautifulsoup4": "bs4",
|
|
28
|
+
"python-dotenv": "dotenv",
|
|
29
|
+
"pymysql": "pymysql",
|
|
30
|
+
"attrs": "attr",
|
|
31
|
+
"google-cloud-storage": "google.cloud.storage",
|
|
32
|
+
"google-auth": "google.auth",
|
|
33
|
+
"python-jose": "jose",
|
|
34
|
+
"python-multipart": "multipart",
|
|
35
|
+
"ujson": "ujson",
|
|
36
|
+
"msgpack": "msgpack",
|
|
37
|
+
"setuptools": ["setuptools", "pkg_resources"],
|
|
38
|
+
"typing-extensions": "typing_extensions",
|
|
39
|
+
"importlib-metadata": "importlib_metadata",
|
|
40
|
+
"importlib-resources": "importlib_resources",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Python tool packages that are run via CLI or plugins
|
|
44
|
+
TOOL_PACKAGES: set[str] = {
|
|
45
|
+
"pip",
|
|
46
|
+
"setuptools",
|
|
47
|
+
"wheel",
|
|
48
|
+
"build",
|
|
49
|
+
"twine",
|
|
50
|
+
"ruff",
|
|
51
|
+
"black",
|
|
52
|
+
"isort",
|
|
53
|
+
"flake8",
|
|
54
|
+
"mypy",
|
|
55
|
+
"pytest",
|
|
56
|
+
"pytest-cov",
|
|
57
|
+
"pytest-xdist",
|
|
58
|
+
"pytest-asyncio",
|
|
59
|
+
"pre-commit",
|
|
60
|
+
"tox",
|
|
61
|
+
"nox",
|
|
62
|
+
"sphinx",
|
|
63
|
+
"ipython",
|
|
64
|
+
"ipdb",
|
|
65
|
+
"debugpy",
|
|
66
|
+
"hatchling",
|
|
67
|
+
"hatch",
|
|
68
|
+
"poetry",
|
|
69
|
+
"flit",
|
|
70
|
+
"pdm",
|
|
71
|
+
"autopep8",
|
|
72
|
+
"pyright",
|
|
73
|
+
"pylint",
|
|
74
|
+
"bandit",
|
|
75
|
+
"safety",
|
|
76
|
+
"coverage",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Node.js tool/build packages that are run via CLI or config
|
|
80
|
+
NODE_TOOL_PACKAGES: set[str] = {
|
|
81
|
+
"typescript",
|
|
82
|
+
"eslint",
|
|
83
|
+
"prettier",
|
|
84
|
+
"vite",
|
|
85
|
+
"webpack",
|
|
86
|
+
"rollup",
|
|
87
|
+
"tsup",
|
|
88
|
+
"esbuild",
|
|
89
|
+
"jest",
|
|
90
|
+
"vitest",
|
|
91
|
+
"mocha",
|
|
92
|
+
"chai",
|
|
93
|
+
"husky",
|
|
94
|
+
"lint-staged",
|
|
95
|
+
"rimraf",
|
|
96
|
+
"nodemon",
|
|
97
|
+
"concurrently",
|
|
98
|
+
"cross-env",
|
|
99
|
+
"tailwindcss",
|
|
100
|
+
"postcss",
|
|
101
|
+
"autoprefixer",
|
|
102
|
+
"dotenv-cli",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Directories to skip when scanning source files
|
|
106
|
+
SKIP_DIRS: set[str] = {
|
|
107
|
+
"venv",
|
|
108
|
+
".venv",
|
|
109
|
+
"node_modules",
|
|
110
|
+
".git",
|
|
111
|
+
"__pycache__",
|
|
112
|
+
".tox",
|
|
113
|
+
".nox",
|
|
114
|
+
"build",
|
|
115
|
+
"dist",
|
|
116
|
+
".next",
|
|
117
|
+
".nuxt",
|
|
118
|
+
".turbo",
|
|
119
|
+
"coverage",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
JS_TS_EXTENSIONS: set[str] = {
|
|
123
|
+
".js",
|
|
124
|
+
".mjs",
|
|
125
|
+
".cjs",
|
|
126
|
+
".jsx",
|
|
127
|
+
".ts",
|
|
128
|
+
".mts",
|
|
129
|
+
".cts",
|
|
130
|
+
".tsx",
|
|
131
|
+
".vue",
|
|
132
|
+
".svelte",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DeadImportScanner:
|
|
137
|
+
"""Detects dependencies declared in config but never imported in code."""
|
|
138
|
+
|
|
139
|
+
name = "dead-import"
|
|
140
|
+
|
|
141
|
+
def scan(self, path: Path) -> list[Ghost]:
|
|
142
|
+
"""Scan a project directory for dead imports across Python and Node.js."""
|
|
143
|
+
declared = self._find_declared_dependencies(path)
|
|
144
|
+
if not declared:
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
imported = self._find_all_imports(path)
|
|
148
|
+
ghosts: list[Ghost] = []
|
|
149
|
+
|
|
150
|
+
for dep_name, source_file in declared:
|
|
151
|
+
is_node = source_file.startswith("package.json")
|
|
152
|
+
|
|
153
|
+
if is_node:
|
|
154
|
+
# Node.js package handling
|
|
155
|
+
if self._is_node_tool_package(dep_name):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
if not self._is_node_dep_used(dep_name, imported):
|
|
159
|
+
ghosts.append(
|
|
160
|
+
Ghost(
|
|
161
|
+
category=GhostCategory.DEAD_IMPORT,
|
|
162
|
+
name=dep_name,
|
|
163
|
+
message=f"Package '{dep_name}' is declared in {source_file} but never imported",
|
|
164
|
+
file_path=path / "package.json",
|
|
165
|
+
severity=Severity.MEDIUM,
|
|
166
|
+
fixable=True,
|
|
167
|
+
suggestion=f"Remove '{dep_name}' from package.json",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
# Python package handling
|
|
172
|
+
normalized = self._normalize_package_name(dep_name)
|
|
173
|
+
if normalized in TOOL_PACKAGES:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
import_names = self._get_import_names(normalized)
|
|
177
|
+
is_used = any(
|
|
178
|
+
self._import_matches(imp, name) for imp in imported for name in import_names
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not is_used:
|
|
182
|
+
ghosts.append(
|
|
183
|
+
Ghost(
|
|
184
|
+
category=GhostCategory.DEAD_IMPORT,
|
|
185
|
+
name=dep_name,
|
|
186
|
+
message=f"Package '{dep_name}' is declared in {source_file} but never imported",
|
|
187
|
+
file_path=path / (source_file.split(" [")[0]),
|
|
188
|
+
severity=Severity.MEDIUM,
|
|
189
|
+
fixable=True,
|
|
190
|
+
suggestion=f"Remove '{dep_name}' from {source_file.split(' [')[0]}",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return ghosts
|
|
195
|
+
|
|
196
|
+
def _is_node_tool_package(self, pkg_name: str) -> bool:
|
|
197
|
+
"""Check if a Node package is a known CLI/type/build tool."""
|
|
198
|
+
if pkg_name.startswith("@types/"):
|
|
199
|
+
return True
|
|
200
|
+
if pkg_name.startswith("@vitejs/") or pkg_name.startswith("@babel/"):
|
|
201
|
+
return True
|
|
202
|
+
return pkg_name.lower() in NODE_TOOL_PACKAGES
|
|
203
|
+
|
|
204
|
+
def _is_node_dep_used(self, dep_name: str, imported: set[str]) -> bool:
|
|
205
|
+
"""Check if a declared Node dependency is used in imports."""
|
|
206
|
+
dep_lower = dep_name.lower()
|
|
207
|
+
for imp in imported:
|
|
208
|
+
imp_lower = imp.lower()
|
|
209
|
+
if imp_lower == dep_lower or imp_lower.startswith(dep_lower + "/"):
|
|
210
|
+
return True
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def _normalize_package_name(self, name: str) -> str:
|
|
214
|
+
"""Normalize a Python package name: lowercase, hyphens to underscores."""
|
|
215
|
+
name = re.split(r"[>=<!\[;@]", name)[0].strip()
|
|
216
|
+
return name.lower().replace("-", "_")
|
|
217
|
+
|
|
218
|
+
def _get_import_names(self, normalized: str) -> list[str]:
|
|
219
|
+
"""Get all possible import names for a normalized package name."""
|
|
220
|
+
original = normalized.replace("_", "-")
|
|
221
|
+
if original in PACKAGE_TO_IMPORT:
|
|
222
|
+
mapping = PACKAGE_TO_IMPORT[original]
|
|
223
|
+
if isinstance(mapping, list):
|
|
224
|
+
return [m.lower().replace("-", "_") for m in mapping]
|
|
225
|
+
return [mapping.lower().replace("-", "_")]
|
|
226
|
+
|
|
227
|
+
if normalized in PACKAGE_TO_IMPORT:
|
|
228
|
+
mapping = PACKAGE_TO_IMPORT[normalized]
|
|
229
|
+
if isinstance(mapping, list):
|
|
230
|
+
return [m.lower().replace("-", "_") for m in mapping]
|
|
231
|
+
return [mapping.lower().replace("-", "_")]
|
|
232
|
+
|
|
233
|
+
return [normalized]
|
|
234
|
+
|
|
235
|
+
def _import_matches(self, imported: str, expected: str) -> bool:
|
|
236
|
+
"""Check if an imported module matches an expected package name."""
|
|
237
|
+
imported_lower = imported.lower().replace("-", "_")
|
|
238
|
+
expected_lower = expected.lower().replace("-", "_")
|
|
239
|
+
return imported_lower == expected_lower or imported_lower.startswith(expected_lower + ".")
|
|
240
|
+
|
|
241
|
+
def _find_declared_dependencies(self, path: Path) -> list[tuple[str, str]]:
|
|
242
|
+
"""Find all declared dependencies from Python and Node.js config files."""
|
|
243
|
+
deps: list[tuple[str, str]] = []
|
|
244
|
+
|
|
245
|
+
# Check requirements.txt
|
|
246
|
+
req_file = path / "requirements.txt"
|
|
247
|
+
if req_file.exists():
|
|
248
|
+
deps.extend(self._parse_requirements_txt(req_file))
|
|
249
|
+
|
|
250
|
+
# Check pyproject.toml
|
|
251
|
+
pyproject = path / "pyproject.toml"
|
|
252
|
+
if pyproject.exists():
|
|
253
|
+
deps.extend(self._parse_pyproject_toml(pyproject))
|
|
254
|
+
|
|
255
|
+
# Check package.json (Node.js)
|
|
256
|
+
package_json = path / "package.json"
|
|
257
|
+
if package_json.exists():
|
|
258
|
+
deps.extend(self._parse_package_json(package_json))
|
|
259
|
+
|
|
260
|
+
return deps
|
|
261
|
+
|
|
262
|
+
def _parse_requirements_txt(self, filepath: Path) -> list[tuple[str, str]]:
|
|
263
|
+
"""Parse a requirements.txt file for package names."""
|
|
264
|
+
deps: list[tuple[str, str]] = []
|
|
265
|
+
try:
|
|
266
|
+
for line in filepath.read_text(encoding="utf-8").splitlines():
|
|
267
|
+
line = line.strip()
|
|
268
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
269
|
+
continue
|
|
270
|
+
name = re.split(r"[>=<!\[;@]", line)[0].strip()
|
|
271
|
+
if name:
|
|
272
|
+
deps.append((name, "requirements.txt"))
|
|
273
|
+
except OSError:
|
|
274
|
+
pass
|
|
275
|
+
return deps
|
|
276
|
+
|
|
277
|
+
def _parse_pyproject_toml(self, filepath: Path) -> list[tuple[str, str]]:
|
|
278
|
+
"""Parse pyproject.toml for declared dependencies."""
|
|
279
|
+
deps: list[tuple[str, str]] = []
|
|
280
|
+
try:
|
|
281
|
+
if sys.version_info >= (3, 11):
|
|
282
|
+
import tomllib
|
|
283
|
+
else:
|
|
284
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
285
|
+
|
|
286
|
+
data = tomllib.loads(filepath.read_text(encoding="utf-8"))
|
|
287
|
+
project_deps = data.get("project", {}).get("dependencies", [])
|
|
288
|
+
for dep in project_deps:
|
|
289
|
+
name = re.split(r"[>=<!\[;@]", dep)[0].strip()
|
|
290
|
+
if name:
|
|
291
|
+
deps.append((name, "pyproject.toml"))
|
|
292
|
+
|
|
293
|
+
# Also check optional dependencies
|
|
294
|
+
optional = data.get("project", {}).get("optional-dependencies", {})
|
|
295
|
+
for group_name, group_deps in optional.items():
|
|
296
|
+
for dep in group_deps:
|
|
297
|
+
name = re.split(r"[>=<!\[;@]", dep)[0].strip()
|
|
298
|
+
if name:
|
|
299
|
+
deps.append((name, f"pyproject.toml [optional: {group_name}]"))
|
|
300
|
+
|
|
301
|
+
# Support Poetry: [tool.poetry.dependencies]
|
|
302
|
+
poetry_deps = data.get("tool", {}).get("poetry", {}).get("dependencies", {})
|
|
303
|
+
if isinstance(poetry_deps, dict):
|
|
304
|
+
for dep_name in poetry_deps:
|
|
305
|
+
if dep_name.lower() != "python":
|
|
306
|
+
deps.append((dep_name, "pyproject.toml [poetry.dependencies]"))
|
|
307
|
+
|
|
308
|
+
# Support Poetry groups: [tool.poetry.group.<group>.dependencies]
|
|
309
|
+
poetry_groups = data.get("tool", {}).get("poetry", {}).get("group", {})
|
|
310
|
+
if isinstance(poetry_groups, dict):
|
|
311
|
+
for group_name, group_data in poetry_groups.items():
|
|
312
|
+
if isinstance(group_data, dict):
|
|
313
|
+
g_deps = group_data.get("dependencies", {})
|
|
314
|
+
if isinstance(g_deps, dict):
|
|
315
|
+
for dep_name in g_deps:
|
|
316
|
+
if dep_name.lower() != "python":
|
|
317
|
+
deps.append(
|
|
318
|
+
(dep_name, f"pyproject.toml [poetry.group.{group_name}]")
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
except (OSError, Exception):
|
|
322
|
+
pass
|
|
323
|
+
return deps
|
|
324
|
+
|
|
325
|
+
def _parse_package_json(self, filepath: Path) -> list[tuple[str, str]]:
|
|
326
|
+
"""Parse package.json for declared dependencies."""
|
|
327
|
+
deps: list[tuple[str, str]] = []
|
|
328
|
+
try:
|
|
329
|
+
data = json.loads(filepath.read_text(encoding="utf-8"))
|
|
330
|
+
for section in ["dependencies", "devDependencies", "peerDependencies"]:
|
|
331
|
+
sec_deps = data.get(section, {})
|
|
332
|
+
if isinstance(sec_deps, dict):
|
|
333
|
+
for dep_name in sec_deps:
|
|
334
|
+
deps.append((dep_name, f"package.json [{section}]"))
|
|
335
|
+
except (OSError, ValueError):
|
|
336
|
+
pass
|
|
337
|
+
return deps
|
|
338
|
+
|
|
339
|
+
def _find_all_imports(self, path: Path) -> set[str]:
|
|
340
|
+
"""Walk Python and JS/TS files and collect all imported module/package names."""
|
|
341
|
+
imports: set[str] = set()
|
|
342
|
+
|
|
343
|
+
for file_path in path.rglob("*"):
|
|
344
|
+
if not file_path.is_file():
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
parts = file_path.relative_to(path).parts
|
|
348
|
+
if any(p in SKIP_DIRS for p in parts):
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
suffix = file_path.suffix.lower()
|
|
352
|
+
|
|
353
|
+
# Python files
|
|
354
|
+
if suffix == ".py":
|
|
355
|
+
self._extract_python_imports(file_path, imports)
|
|
356
|
+
# JS/TS/Vue/Svelte files
|
|
357
|
+
elif suffix in JS_TS_EXTENSIONS:
|
|
358
|
+
self._extract_js_ts_imports(file_path, imports)
|
|
359
|
+
|
|
360
|
+
return imports
|
|
361
|
+
|
|
362
|
+
def _extract_python_imports(self, filepath: Path, imports: set[str]) -> None:
|
|
363
|
+
"""Extract imports from a Python file."""
|
|
364
|
+
try:
|
|
365
|
+
source = filepath.read_text(encoding="utf-8", errors="ignore")
|
|
366
|
+
tree = ast.parse(source, filename=str(filepath))
|
|
367
|
+
for node in ast.walk(tree):
|
|
368
|
+
if isinstance(node, ast.Import):
|
|
369
|
+
for alias in node.names:
|
|
370
|
+
imports.add(alias.name)
|
|
371
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
372
|
+
imports.add(node.module)
|
|
373
|
+
except (SyntaxError, OSError):
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
def _extract_js_ts_imports(self, filepath: Path, imports: set[str]) -> None:
|
|
377
|
+
"""Extract imports/requires from a JavaScript or TypeScript file."""
|
|
378
|
+
try:
|
|
379
|
+
source = filepath.read_text(encoding="utf-8", errors="ignore")
|
|
380
|
+
|
|
381
|
+
# Patterns:
|
|
382
|
+
# 1. import ... from 'pkg'
|
|
383
|
+
# 2. import 'pkg'
|
|
384
|
+
# 3. export ... from 'pkg'
|
|
385
|
+
# 4. require('pkg')
|
|
386
|
+
# 5. import('pkg')
|
|
387
|
+
patterns = [
|
|
388
|
+
r"""(?:import|export)\s+(?:[\w\s{},*]+from\s+)?['"]([^'"]+)['"]""",
|
|
389
|
+
r"""(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)""",
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
for pattern in patterns:
|
|
393
|
+
for match in re.finditer(pattern, source):
|
|
394
|
+
raw_specifier = match.group(1).strip()
|
|
395
|
+
# Skip relative or absolute paths
|
|
396
|
+
if (
|
|
397
|
+
raw_specifier.startswith(".")
|
|
398
|
+
or raw_specifier.startswith("/")
|
|
399
|
+
or raw_specifier.startswith("#")
|
|
400
|
+
):
|
|
401
|
+
continue
|
|
402
|
+
|
|
403
|
+
# Handle scoped package: @org/pkg/sub -> @org/pkg
|
|
404
|
+
if raw_specifier.startswith("@"):
|
|
405
|
+
parts = raw_specifier.split("/")
|
|
406
|
+
if len(parts) >= 2:
|
|
407
|
+
imports.add(f"{parts[0]}/{parts[1]}")
|
|
408
|
+
else:
|
|
409
|
+
imports.add(raw_specifier)
|
|
410
|
+
else:
|
|
411
|
+
# Unscoped package: pkg/sub -> pkg
|
|
412
|
+
pkg_root = raw_specifier.split("/")[0]
|
|
413
|
+
imports.add(pkg_root)
|
|
414
|
+
|
|
415
|
+
except OSError:
|
|
416
|
+
pass
|
|
@@ -47,6 +47,9 @@ IGNORABLE_PATTERNS: dict[str, str] = {
|
|
|
47
47
|
"Thumbs.db": "Windows thumbnail cache",
|
|
48
48
|
".env": "Environment variables file (may contain secrets)",
|
|
49
49
|
".env.local": "Local environment variables",
|
|
50
|
+
"npm-debug.log": "NPM debug log file",
|
|
51
|
+
"yarn-error.log": "Yarn error log file",
|
|
52
|
+
"pnpm-debug.log": "pnpm debug log file",
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
# Size threshold for flagging large files (10 MB)
|