From c1274402a04d798876df85ef519b5a076910c6f9 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 21:35:24 +0000 Subject: [PATCH] Add license and unused dependency checks --- depaudit/checks/unused.py | 210 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 depaudit/checks/unused.py diff --git a/depaudit/checks/unused.py b/depaudit/checks/unused.py new file mode 100644 index 0000000..fad38d8 --- /dev/null +++ b/depaudit/checks/unused.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +import ast +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +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 "" + names = [alias.asname or alias.name for alias in node.names] + 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