from __future__ import annotations import json import os import re import xml.etree.ElementTree as ET from typing import List, Dict, Any, Optional from .toml_compat import loads as toml_loads def _xml_text(elem: ET.Element, tag: str) -> Optional[str]: def ns_strip(s: str) -> str: return s.split("}", 1)[-1] if "}" in s else s for child in elem: if ns_strip(child.tag) == tag and child.text: return child.text.strip() return None def parse_requirements_txt(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] for line in text.splitlines(): line = line.strip() if not line or line.startswith("#"): continue if "://" in line or line.startswith("git+"): deps.append({"ecosystem": "pypi", "name": line, "spec": None, "note": "non-standard", "scope": "requirements"}) continue m = re.match(r"^([A-Za-z0-9_.-]+)\s*([<>=!~]=?)\s*([^\s;]+)", line) if m: name, op, ver = m.group(1), m.group(2), m.group(3) deps.append({"ecosystem": "pypi", "name": name, "spec": f"{op}{ver}", "scope": "requirements"}) else: deps.append({"ecosystem": "pypi", "name": line, "spec": None, "scope": "requirements"}) return deps def parse_pipfile(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] data = toml_loads(text) for section, scope in (("packages", "dependencies"), ("dev-packages", "devDependencies")): block = data.get(section, {}) or {} for name, spec in block.items(): if isinstance(spec, str): deps.append({"ecosystem": "pypi", "name": name, "spec": spec, "scope": scope}) elif isinstance(spec, dict): deps.append({"ecosystem": "pypi", "name": name, "spec": spec.get("version"), "scope": scope}) else: deps.append({"ecosystem": "pypi", "name": name, "spec": None, "scope": scope}) return deps def parse_pyproject_toml(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] data = toml_loads(text) # PEP 621 proj = data.get("project", {}) or {} for item in proj.get("dependencies", []) or []: if isinstance(item, str): deps.append({"ecosystem": "pypi", "name": item, "spec": None, "scope": "project.dependencies"}) opt = proj.get("optional-dependencies", {}) or {} for group, items in opt.items(): for item in items or []: if isinstance(item, str): deps.append({"ecosystem": "pypi", "name": item, "spec": None, "scope": f"optional:{group}"}) # Poetry tool = data.get("tool", {}) or {} poetry = (tool.get("poetry", {}) or {}) for section, scope in (("dependencies", "poetry.dependencies"), ("dev-dependencies", "poetry.dev-dependencies")): block = poetry.get(section, {}) or {} for name, spec in block.items(): if name.lower() == "python": continue if isinstance(spec, str): deps.append({"ecosystem": "pypi", "name": name, "spec": spec, "scope": scope}) elif isinstance(spec, dict): deps.append({"ecosystem": "pypi", "name": name, "spec": spec.get("version"), "scope": scope}) else: deps.append({"ecosystem": "pypi", "name": name, "spec": None, "scope": scope}) group = poetry.get("group", {}) or {} for gname, gdata in group.items(): block = (gdata or {}).get("dependencies", {}) or {} for name, spec in block.items(): if name.lower() == "python": continue if isinstance(spec, str): deps.append({"ecosystem": "pypi", "name": name, "spec": spec, "scope": f"poetry.group.{gname}"}) elif isinstance(spec, dict): deps.append({"ecosystem": "pypi", "name": name, "spec": spec.get("version"), "scope": f"poetry.group.{gname}"}) else: deps.append({"ecosystem": "pypi", "name": name, "spec": None, "scope": f"poetry.group.{gname}"}) return deps def parse_poetry_lock(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] data = toml_loads(text) pkgs = data.get("package", []) or [] for p in pkgs: name = p.get("name") ver = p.get("version") if name and ver: deps.append({"ecosystem": "pypi", "name": name, "spec": ver, "scope": "lock"}) return deps def parse_package_json(text: str) -> List[Dict[str, Any]]: data = json.loads(text) deps: List[Dict[str, Any]] = [] for section in ("dependencies", "devDependencies", "optionalDependencies", "peerDependencies"): block = data.get(section, {}) or {} for name, spec in block.items(): deps.append({"ecosystem": "npm", "name": name, "spec": spec, "scope": section}) return deps def parse_package_lock_json(text: str) -> List[Dict[str, Any]]: data = json.loads(text) deps: List[Dict[str, Any]] = [] if isinstance(data.get("packages"), dict): for k, v in data["packages"].items(): if not k or not k.startswith("node_modules/"): continue name = k[len("node_modules/"):] ver = (v or {}).get("version") if name and ver: deps.append({"ecosystem": "npm", "name": name, "spec": ver, "scope": "lock"}) return deps if isinstance(data.get("dependencies"), dict): for name, v in data["dependencies"].items(): ver = (v or {}).get("version") if name and ver: deps.append({"ecosystem": "npm", "name": name, "spec": ver, "scope": "lock"}) return deps return deps def parse_yarn_lock(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] current_key: Optional[str] = None for raw in text.splitlines(): line = raw.rstrip() if not line: continue if not line.startswith(" ") and line.endswith(":"): key = line[:-1].strip().strip('"').strip("'") first = key.split(",")[0].strip().strip('"').strip("'") current_key = first continue if current_key and line.strip().startswith("version "): m = re.match(r'^\s*version\s+"([^"]+)"\s*$', line) if m: ver = m.group(1) last_at = current_key.rfind("@") if last_at > 0: name = current_key[:last_at] deps.append({"ecosystem": "npm", "name": name, "spec": ver, "scope": "lock"}) current_key = None return deps def parse_pnpm_lock_yaml(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] rx = re.compile(r'^\s*/(.+?)@([0-9][^:\s]+):\s*$', re.MULTILINE) for m in rx.finditer(text): name = m.group(1).strip() ver = m.group(2).strip() if name and ver: deps.append({"ecosystem": "npm", "name": name, "spec": ver, "scope": "lock"}) return deps def parse_go_mod(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] in_require = False for raw in text.splitlines(): line = raw.strip() if not line or line.startswith("//"): continue if line.startswith("require ("): in_require = True continue if in_require and line == ")": in_require = False continue if line.startswith("require "): line = line[len("require "):].strip() parts = line.split() if len(parts) >= 2: name, ver = parts[0], parts[1] if name == "go": continue deps.append({"ecosystem": "go", "name": name, "spec": ver, "scope": "require"}) return deps def parse_go_sum(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] seen = set() for raw in text.splitlines(): line = raw.strip() if not line: continue parts = line.split() if len(parts) >= 2: mod, ver = parts[0], parts[1] ver = ver.replace("/go.mod", "") key = (mod, ver) if key in seen: continue seen.add(key) deps.append({"ecosystem": "go", "name": mod, "spec": ver, "scope": "sum"}) return deps def parse_cargo_toml(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] data = toml_loads(text) for section, scope in (("dependencies", "dependencies"), ("dev-dependencies", "dev-dependencies"), ("build-dependencies", "build-dependencies")): block = data.get(section, {}) or {} for name, spec in block.items(): if isinstance(spec, str): deps.append({"ecosystem": "cargo", "name": name, "spec": spec, "scope": scope}) elif isinstance(spec, dict): deps.append({"ecosystem": "cargo", "name": name, "spec": spec.get("version"), "scope": scope}) else: deps.append({"ecosystem": "cargo", "name": name, "spec": None, "scope": scope}) return deps def parse_cargo_lock(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] data = toml_loads(text) pkgs = data.get("package", []) or [] for p in pkgs: name = p.get("name") ver = p.get("version") if name and ver: deps.append({"ecosystem": "cargo", "name": name, "spec": ver, "scope": "lock"}) return deps def parse_pom_xml(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] try: root = ET.fromstring(text) except ET.ParseError: return deps for dep in root.iter(): if not dep.tag.endswith("dependency"): continue gid = _xml_text(dep, "groupId") aid = _xml_text(dep, "artifactId") ver = _xml_text(dep, "version") scope = _xml_text(dep, "scope") or "compile" if gid and aid: deps.append({"ecosystem": "maven", "name": f"{gid}:{aid}", "spec": ver, "scope": scope}) return deps def parse_gradle_build(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] rx = re.compile( r"^\s*(implementation|api|compileOnly|runtimeOnly|testImplementation|testCompileOnly)\s*\(?\s*['\"]([^'\"]+)['\"]\s*\)?\s*$", re.MULTILINE, ) for m in rx.finditer(text): scope = m.group(1) gav = m.group(2).strip() parts = gav.split(":") if len(parts) >= 2: name = f"{parts[0]}:{parts[1]}" ver = parts[2] if len(parts) >= 3 else None deps.append({"ecosystem": "gradle", "name": name, "spec": ver, "scope": scope}) return deps def parse_csproj(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] try: root = ET.fromstring(text) except ET.ParseError: return deps for elem in root.iter(): if not elem.tag.endswith("PackageReference"): continue name = elem.attrib.get("Include") or elem.attrib.get("Update") ver = elem.attrib.get("Version") if ver is None: for ch in elem: if ch.tag.endswith("Version") and ch.text: ver = ch.text.strip() break if name: deps.append({"ecosystem": "nuget", "name": name, "spec": ver, "scope": "csproj"}) return deps def parse_packages_config(text: str) -> List[Dict[str, Any]]: deps: List[Dict[str, Any]] = [] try: root = ET.fromstring(text) except ET.ParseError: return deps for pkg in root.findall(".//package"): name = pkg.attrib.get("id") ver = pkg.attrib.get("version") if name: deps.append({"ecosystem": "nuget", "name": name, "spec": ver, "scope": "packages.config"}) return deps def parse_manifest_by_name(filename: str, text: str) -> List[Dict[str, Any]]: base = os.path.basename(filename) if base.startswith("requirements") and base.endswith(".txt"): return parse_requirements_txt(text) if base == "Pipfile": return parse_pipfile(text) if base == "pyproject.toml": return parse_pyproject_toml(text) if base == "poetry.lock": return parse_poetry_lock(text) if base == "package.json": return parse_package_json(text) if base == "package-lock.json": return parse_package_lock_json(text) if base == "yarn.lock": return parse_yarn_lock(text) if base == "pnpm-lock.yaml": return parse_pnpm_lock_yaml(text) if base == "go.mod": return parse_go_mod(text) if base == "go.sum": return parse_go_sum(text) if base == "Cargo.toml": return parse_cargo_toml(text) if base == "Cargo.lock": return parse_cargo_lock(text) if base == "pom.xml": return parse_pom_xml(text) if base in ("build.gradle", "build.gradle.kts"): return parse_gradle_build(text) if base.endswith(".csproj") or base.endswith(".fsproj"): return parse_csproj(text) if base == "packages.config": return parse_packages_config(text) return []