Files
depaudit-cli/depaudit/checks/unused.py
7000pctAUTO 6611aa6fd8
Some checks failed
CI / test (push) Has been cancelled
fix: resolve CI linting failures
2026-02-02 21:48:01 +00:00

209 lines
6.8 KiB
Python

from __future__ import annotations
import ast
import re
from dataclasses import dataclass
from pathlib import Path
from depaudit.checks import UnusedDependency
@dataclass
class ImportStatement:
module_name: str
names: list[str]
line_number: int
is_from: bool
alias: str | None = None
class SourceParser:
def __init__(self, language: str):
self.language = language
def parse_file(self, file_path: Path) -> list[ImportStatement]:
raise NotImplementedError
class PythonSourceParser(SourceParser):
def __init__(self):
super().__init__("python")
def parse_file(self, file_path: Path) -> list[ImportStatement]:
imports = []
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(
ImportStatement(
module_name=alias.name,
names=[alias.asname or alias.name],
line_number=node.lineno,
is_from=False,
alias=alias.asname,
)
)
elif isinstance(node, ast.ImportFrom):
module = node.module or ""
for alias in node.names:
imports.append(
ImportStatement(
module_name=f"{module}.{alias.name}" if module else alias.name,
names=[alias.asname or alias.name],
line_number=node.lineno,
is_from=True,
alias=alias.asname,
)
)
except Exception:
pass
return imports
class JavaScriptSourceParser(SourceParser):
def __init__(self):
super().__init__("javascript")
def parse_file(self, file_path: Path) -> list[ImportStatement]:
imports = []
content = file_path.read_text(encoding="utf-8")
import_patterns = [
(r"import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+[\"\']([^\"\']+)[\"\']", True),
(r"import\s+[\"\']([^\"\']+)[\"\']", True),
(r"require\s*\(\s*[\"\']([^\"\']+)[\"\']\s*\)", False),
]
for pattern, is_from in import_patterns:
for match in re.finditer(pattern, content):
module_name = match.group(1)
imports.append(
ImportStatement(
module_name=module_name,
names=[module_name.split("/")[-1].split("@")[0]],
line_number=content[:match.start()].count("\n") + 1,
is_from=is_from,
)
)
return imports
class GoSourceParser(SourceParser):
def __init__(self):
super().__init__("go")
def parse_file(self, file_path: Path) -> list[ImportStatement]:
imports = []
content = file_path.read_text(encoding="utf-8")
import_pattern = r"import\s*\(([^)]+)\)"
block_match = re.search(import_pattern, content, re.DOTALL)
if block_match:
block = block_match.group(1)
for line in block.split("\n"):
import_match = re.search(r'"([^"]+)"', line)
if import_match:
module_name = import_match.group(1)
imports.append(
ImportStatement(
module_name=module_name,
names=[module_name.split("/")[-1]],
line_number=content[:block_match.start()].count("\n") + 1,
is_from=True,
)
)
inline_imports = re.findall(r'\bimport\s+[\"\']([^\"\']+)[\"\']', content)
for module_name in inline_imports:
imports.append(
ImportStatement(
module_name=module_name,
names=[module_name.split("/")[-1]],
line_number=1,
is_from=True,
)
)
return imports
PARSERS = {
"python": PythonSourceParser,
"javascript": JavaScriptSourceParser,
"go": GoSourceParser,
}
def get_source_parser(language: str) -> SourceParser | None:
parser_class = PARSERS.get(language)
if parser_class:
return parser_class()
return None
def check_unused_dependencies(
dependencies: list[tuple[str, str]],
source_dir: Path,
language: str,
exclude_patterns: list[str] | None = None,
) -> list[UnusedDependency]:
exclude_patterns = exclude_patterns or []
parser = get_source_parser(language)
if parser is None:
return []
all_imports: set[str] = set()
for file_path in source_dir.rglob("*"):
if file_path.is_file():
skip = False
for pattern in exclude_patterns:
if file_path.match(pattern):
skip = True
break
if skip:
continue
if language == "python" and file_path.suffix == ".py":
file_imports = parser.parse_file(file_path)
all_imports.update(imp.module_name.split(".")[0] for imp in file_imports)
elif language == "javascript" and file_path.suffix in (".js", ".jsx", ".ts", ".tsx"):
file_imports = parser.parse_file(file_path)
all_imports.update(imp.module_name.split("/")[0].split("@")[0] for imp in file_imports)
elif language == "go" and file_path.suffix == ".go":
file_imports = parser.parse_file(file_path)
all_imports.update(imp.module_name.split("/")[-1] for imp in file_imports)
unused = []
for name, version in dependencies:
normalized_name = name.lower().replace("_", "-").split("/")[-1].split("@")[0]
found = False
for imp in all_imports:
normalized_imp = imp.lower().replace("_", "-")
if normalized_name in normalized_imp or normalized_imp in normalized_name:
found = True
break
if not found:
unused.append(
UnusedDependency(
package_name=name,
version=version,
language=language,
declared_in="dependency file",
file_path=str(source_dir),
reason=f"Package '{name}' is declared but never imported or required",
)
)
return unused