from __future__ import annotations import json from pathlib import Path from typing import Any from depaudit.parsers import Parser, ParsedManifest, Dependency class JavaScriptParser(Parser): language = "javascript" def can_parse(self, file_path: Path) -> bool: return file_path.name in ("package.json", "package-lock.json", "yarn.lock") def parse(self, file_path: Path) -> ParsedManifest: with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) manifest = ParsedManifest( language=self.language, file_path=file_path, project_name=data.get("name"), project_version=data.get("version"), raw_data=data, ) if file_path.name == "package.json": self._parse_package_json(file_path, data, manifest) elif file_path.name == "package-lock.json": self._parse_package_lock(file_path, data, manifest) elif file_path.name == "yarn.lock": self._parse_yarn_lock(file_path, data, manifest) return manifest def _parse_package_json( self, file_path: Path, data: dict[str, Any], manifest: ParsedManifest ) -> None: dependencies = data.get("dependencies", {}) dev_dependencies = data.get("devDependencies", {}) optional_dependencies = data.get("optionalDependencies", {}) for name, version in dependencies.items(): manifest.dependencies.append( self._create_dependency(file_path, name, version, dev=False) ) for name, version in dev_dependencies.items(): manifest.dependencies.append( self._create_dependency(file_path, name, version, dev=True) ) for name, version in optional_dependencies.items(): manifest.dependencies.append( self._create_dependency(file_path, name, version, optional=True) ) def _parse_package_lock( self, file_path: Path, data: dict[str, Any], manifest: ParsedManifest ) -> None: packages = data.get("packages", {}) dependencies = data.get("dependencies", {}) for name, info in dependencies.items(): version = info.get("version", "") dev = info.get("dev", False) optional = info.get("optional", False) link = info.get("link", False) if not link: manifest.dependencies.append( self._create_dependency( file_path, name, version, dev=dev, optional=optional, ) ) for name, info in packages.items(): if name == "": continue version = info.get("version", "") dev = info.get("dev", False) optional = info.get("optional", False) manifest.dependencies.append( self._create_dependency( file_path, name, version, dev=dev, optional=optional, indirect=False, ) ) def _parse_yarn_lock(self, file_path: Path, data: str, manifest: ParsedManifest) -> None: lines = data.split("\n") current_package = None current_version = None in_package = False for line in lines: if line.startswith("@") or line.startswith("?"): continue if not in_package and ":" in line: parts = line.split(":", 1) current_package = parts[0].strip().replace('"', "") if len(parts) > 1: current_version = parts[1].strip().split()[0].replace('"', "") in_package = True elif in_package and line.startswith("version") and current_package: version_line = line.split(":", 1) if len(version_line) > 1: current_version = version_line[1].strip().replace('"', "") elif in_package and (line.startswith("resolved") or line.strip() == ""): if current_package and current_version: manifest.dependencies.append( self._create_dependency(file_path, current_package, current_version) ) in_package = False current_package = None current_version = None if current_package and current_version: manifest.dependencies.append( self._create_dependency(file_path, current_package, current_version) )