import type { Dependency } from "./manifests-json"; const REQUIREMENT_LINE = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?\s*(.*)$/; function pep508(spec: string, scope: Dependency["scope"], direct: boolean): Dependency | null { const clean = spec.split(";")[0].trim(); const match = REQUIREMENT_LINE.exec(clean); if (!match) return null; return { name: match[1].toLowerCase(), requirement: match[2].trim() || null, scope, direct }; } export function parseRequirementsTxt(text: string, scope: Dependency["scope"]): Dependency[] { return text.split("\n").flatMap((raw) => { const line = raw.split(" #")[0].trim(); if (!line || line.startsWith("#") || line.startsWith("-") || line.includes("://")) return []; const dependency = pep508(line, scope, true); return dependency ? [dependency] : []; }); } type TomlTable = { name: string; lines: string[] }; /** Linear comment stripper: `\s+#.*$` backtracks quadratically on long runs of whitespace (INT-A10). */ export function stripTomlComment(raw: string): string { let quote: string | null = null; for (let index = 0; index < raw.length; index++) { const char = raw[index]; if (quote) { if (char === "\\" && quote === '"') index++; else if (char === quote) quote = null; } else if (char === '"' || char === "'") quote = char; else if (char === "#") return raw.slice(0, index); } return raw; } /** Splits TOML into tables by header; enough for dependency sections without a full TOML parser. */ function tomlTables(text: string): TomlTable[] { const tables: TomlTable[] = [{ name: "", lines: [] }]; for (const raw of text.split("\n")) { const line = stripTomlComment(raw).trim(); const header = /^\[\[?([^\]]+)\]\]?$/.exec(line); if (header) tables.push({ name: header[1].trim(), lines: [] }); else if (line && !line.startsWith("#")) tables[tables.length - 1].lines.push(line); } return tables; } function tomlString(value: string): string | null { const match = /^["']([^"']*)["']/.exec(value.trim()); return match ? match[1] : null; } function tomlInlineVersion(value: string): string | null { const direct = tomlString(value); if (direct !== null) return direct; const version = /version\s*=\s*["']([^"']*)["']/.exec(value); return version ? version[1] : null; } function keyValues(lines: string[]): Array<[string, string]> { return lines.flatMap((line) => { const equals = line.indexOf("="); if (equals === -1) return []; return [ [ line .slice(0, equals) .trim() .replace(/^["']|["']$/g, ""), line.slice(equals + 1).trim(), ], ]; }); } /** Collects a multi-line TOML string array such as `dependencies = [ ... ]`. */ function tomlArray(lines: string[], key: string): string[] { const start = lines.findIndex((line) => line.startsWith(key) && /^[\w-]+\s*=\s*\[/.test(line)); if (start === -1) return []; let joined = ""; for (let index = start; index < lines.length; index++) { joined += lines[index]; if (lines[index].includes("]")) break; } return [...joined.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]); } export function parsePyproject(text: string): Dependency[] { const out: Dependency[] = []; for (const table of tomlTables(text)) { if (table.name === "project") { for (const spec of tomlArray(table.lines, "dependencies")) { const dependency = pep508(spec, "runtime", true); if (dependency) out.push(dependency); } } else if (table.name.startsWith("project.optional-dependencies") || table.name === "dependency-groups") { for (const [, value] of keyValues(table.lines)) { for (const match of value.matchAll(/["']([^"']+)["']/g)) { const dependency = pep508(match[1], "development", true); if (dependency) out.push(dependency); } } } else if ( table.name === "tool.poetry.dependencies" || /^tool\.poetry\.(dev-dependencies|group\.[^.]+\.dependencies)$/.test(table.name) ) { const scope = table.name === "tool.poetry.dependencies" ? "runtime" : "development"; for (const [name, value] of keyValues(table.lines)) { if (name === "python") continue; out.push({ name: name.toLowerCase(), requirement: tomlInlineVersion(value), scope, direct: true }); } } } return out; } export function parseCargoToml(text: string): Dependency[] { const out: Dependency[] = []; for (const table of tomlTables(text)) { const section = /(?:^|\.)(dependencies|dev-dependencies|build-dependencies)$/.exec(table.name)?.[1]; if (!section) continue; for (const [name, value] of keyValues(table.lines)) { out.push({ name, requirement: tomlInlineVersion(value), scope: section === "dependencies" ? "runtime" : "development", direct: true, }); } } return out; } export function parseCargoLock(text: string): Dependency[] { return tomlTables(text) .filter((table) => table.name === "package") .flatMap((table) => { const values = new Map(keyValues(table.lines)); const name = tomlString(values.get("name") ?? ""); if (!name || values.has("source") === false) return []; return [{ name, requirement: tomlString(values.get("version") ?? ""), scope: "runtime" as const, direct: false }]; }); } export function parseGoMod(text: string): Dependency[] { const out: Dependency[] = []; let inBlock = false; for (const raw of text.split("\n")) { const line = raw.trim(); if (line.startsWith("require (")) { inBlock = true; continue; } if (inBlock && line === ")") { inBlock = false; continue; } const body = inBlock ? line : line.startsWith("require ") ? line.slice("require ".length).trim() : null; if (!body || body.startsWith("//")) continue; const [name, version] = body.split(/\s+/); if (!name || !version) continue; out.push({ name, requirement: version, scope: "runtime", direct: !body.includes("// indirect") }); } return out; } export function parseGemfileLock(text: string): Dependency[] { const direct = new Set(); const specs: Dependency[] = []; let section = ""; let inSpecs = false; for (const line of text.split("\n")) { if (/^[A-Z]/.test(line)) { section = line.trim(); inSpecs = false; continue; } if (line.trim() === "specs:") { inSpecs = true; continue; } const spec = /^ {4}([^\s(]+) \(([^)]+)\)$/.exec(line); if (section === "GEM" && inSpecs && spec) { specs.push({ name: spec[1], requirement: spec[2], scope: "runtime", direct: false }); } const dependency = /^ {2}([^\s(!]+)/.exec(line); if (section === "DEPENDENCIES" && dependency) direct.add(dependency[1]); } return specs.map((spec) => ({ ...spec, direct: direct.has(spec.name) })); }