"""Parse dependency files from common package managers.""" from __future__ import annotations import json import os import re import xml.etree.ElementTree as ET import toml import yaml def parse_dependencies(repo_path: str) -> dict: deps = {"npm": [], "pip": [], "maven": [], "gem": [], "go": [], "cargo": [], "composer": []} deps["npm"] = _parse_package_json(os.path.join(repo_path, "package.json")) deps["pip"] = _parse_requirements(os.path.join(repo_path, "requirements.txt")) deps["pip"].extend(_parse_pyproject(os.path.join(repo_path, "pyproject.toml"))) deps["maven"] = _parse_pom(os.path.join(repo_path, "pom.xml")) deps["gem"] = _parse_gemfile(os.path.join(repo_path, "Gemfile")) deps["go"] = _parse_go_mod(os.path.join(repo_path, "go.mod")) deps["cargo"] = _parse_cargo(os.path.join(repo_path, "Cargo.toml")) deps["composer"] = _parse_composer(os.path.join(repo_path, "composer.json")) return {key: sorted(set(value)) for key, value in deps.items()} def _parse_package_json(path: str) -> list[str]: if not os.path.exists(path): return [] try: with open(path, "r", encoding="utf-8") as file: pkg = json.load(file) merged = {} for key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"): merged.update(pkg.get(key, {})) return list(merged.keys()) except (OSError, json.JSONDecodeError): return [] def _parse_requirements(path: str) -> list[str]: if not os.path.exists(path): return [] deps = [] try: with open(path, "r", encoding="utf-8", errors="ignore") as file: for raw_line in file: line = raw_line.strip() if not line or line.startswith("#") or line.startswith(("-r", "--")): continue package = re.split(r"[<>=~!\[]", line, maxsplit=1)[0].strip() if package: deps.append(package) except OSError: return [] return deps def _parse_pyproject(path: str) -> list[str]: if not os.path.exists(path): return [] try: data = toml.load(path) except Exception: return [] deps = [] project = data.get("project", {}) for dep in project.get("dependencies", []): package = re.split(r"[<>=~!\[]", dep, maxsplit=1)[0].strip() if package: deps.append(package) poetry_deps = data.get("tool", {}).get("poetry", {}).get("dependencies", {}) deps.extend(key for key in poetry_deps.keys() if key.lower() != "python") return deps def _parse_pom(path: str) -> list[str]: if not os.path.exists(path): return [] try: tree = ET.parse(path) root = tree.getroot() deps = [] for dependency in root.findall(".//{*}dependency"): group = dependency.find("{*}groupId") artifact = dependency.find("{*}artifactId") if artifact is not None: deps.append(f"{group.text if group is not None else ''}:{artifact.text}".strip(":")) return deps except Exception: return [] def _parse_gemfile(path: str) -> list[str]: if not os.path.exists(path): return [] deps = [] try: with open(path, "r", encoding="utf-8", errors="ignore") as file: for line in file: match = re.match(r"\s*gem\s+['\"]([^'\"]+)['\"]", line) if match: deps.append(match.group(1)) except OSError: return [] return deps def _parse_go_mod(path: str) -> list[str]: if not os.path.exists(path): return [] deps = [] try: with open(path, "r", encoding="utf-8", errors="ignore") as file: for line in file: stripped = line.strip() if not stripped or stripped.startswith(("module ", "go ", "//", "require (", ")")): continue if stripped.startswith("require "): parts = stripped.split() if len(parts) >= 2: deps.append(parts[1]) elif "/" in stripped: deps.append(stripped.split()[0]) except OSError: return [] return deps def _parse_cargo(path: str) -> list[str]: if not os.path.exists(path): return [] try: data = toml.load(path) return list(data.get("dependencies", {}).keys()) + list(data.get("dev-dependencies", {}).keys()) except Exception: return [] def _parse_composer(path: str) -> list[str]: if not os.path.exists(path): return [] try: with open(path, "r", encoding="utf-8") as file: data = json.load(file) merged = {} merged.update(data.get("require", {})) merged.update(data.get("require-dev", {})) return [key for key in merged.keys() if key != "php"] except (OSError, json.JSONDecodeError): return [] def parse_yaml_file(path: str) -> dict: if not os.path.exists(path): return {} try: with open(path, "r", encoding="utf-8", errors="ignore") as file: return yaml.safe_load(file) or {} except Exception: return {}