diff --git a/app/src/analyzers/dependencyAnalyzer.ts b/app/src/analyzers/dependencyAnalyzer.ts new file mode 100644 index 0000000..e7d0ee4 --- /dev/null +++ b/app/src/analyzers/dependencyAnalyzer.ts @@ -0,0 +1,263 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { DependencyAnalysis, DependencyInfo, ProjectType } from '../types'; +import { parseJSONFile, parseTOMLFile } from '../utils/fileUtils'; + +const NODE_PACKAGE_MANAGERS = ['yarn.lock', 'pnpm-lock.yaml', 'bun.lockb']; +const PYTHON_PACKAGE_MANAGERS = ['Pipfile.lock', 'poetry.lock']; + +export async function analyzeDependencies( + directory: string, + projectType: ProjectType +): Promise { + let dependencies: DependencyInfo[] = []; + let packageManager: DependencyAnalysis['packageManager'] = null; + let lockFile: string | undefined; + + switch (projectType) { + case 'node': + const nodeDeps = await analyzeNodeDependencies(directory); + dependencies = nodeDeps.dependencies; + packageManager = nodeDeps.packageManager; + lockFile = nodeDeps.lockFile; + break; + case 'python': + const pyDeps = await analyzePythonDependencies(directory); + dependencies = pyDeps.dependencies; + packageManager = pyDeps.packageManager; + lockFile = pyDeps.lockFile; + break; + case 'go': + const goDeps = await analyzeGoDependencies(directory); + dependencies = goDeps.dependencies; + packageManager = goDeps.packageManager; + break; + case 'rust': + const rustDeps = await analyzeRustDependencies(directory); + dependencies = rustDeps.dependencies; + packageManager = rustDeps.packageManager; + break; + case 'java': + const javaDeps = await analyzeJavaDependencies(directory); + dependencies = javaDeps.dependencies; + packageManager = javaDeps.packageManager; + break; + default: + dependencies = []; + } + + return { + dependencies, + packageManager, + lockFile + }; +} + +async function analyzeNodeDependencies( + directory: string +): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager']; lockFile?: string }> { + const packageJsonPath = path.join(directory, 'package.json'); + const packageJson = parseJSONFile<{ + dependencies?: Record; + devDependencies?: Record; + }>(packageJsonPath); + + if (!packageJson) { + return { dependencies: [], packageManager: null }; + } + + const dependencies: DependencyInfo[] = []; + + if (packageJson.dependencies) { + for (const [name, version] of Object.entries(packageJson.dependencies)) { + dependencies.push({ name, version: String(version), type: 'prod' }); + } + } + + if (packageJson.devDependencies) { + for (const [name, version] of Object.entries(packageJson.devDependencies)) { + dependencies.push({ name, version: String(version), type: 'dev' }); + } + } + + let packageManager: DependencyAnalysis['packageManager'] = 'npm'; + let lockFile: string | undefined; + + for (const lockFileName of NODE_PACKAGE_MANAGERS) { + if (fs.existsSync(path.join(directory, lockFileName))) { + lockFile = lockFileName; + if (lockFileName === 'yarn.lock') packageManager = 'yarn'; + else if (lockFileName === 'pnpm-lock.yaml') packageManager = 'pnpm'; + else if (lockFileName === 'bun.lockb') packageManager = 'npm'; + break; + } + } + + return { dependencies, packageManager, lockFile }; +} + +async function analyzePythonDependencies( + directory: string +): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager']; lockFile?: string }> { + const dependencies: DependencyInfo[] = []; + + const requirementsPath = path.join(directory, 'requirements.txt'); + if (fs.existsSync(requirementsPath)) { + const content = fs.readFileSync(requirementsPath, 'utf-8'); + const lines = content.split('\n'); + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+[^;]+)?/); + if (match) { + dependencies.push({ + name: match[1], + version: match[2] || 'latest', + type: 'prod' + }); + } + } + } + } + + const pyprojectPath = path.join(directory, 'pyproject.toml'); + const pyproject = parseTOMLFile(pyprojectPath); + + const projectDeps = pyproject?.['project'] as Record | undefined; + if (projectDeps && Array.isArray(projectDeps['dependencies'])) { + const deps = projectDeps['dependencies'] as string[]; + for (const dep of deps) { + const match = dep.match(/^([a-zA-Z0-9_-]+)([<>=!~]+[^;]+)?/); + if (match) { + dependencies.push({ + name: match[1], + version: match[2] || 'latest', + type: 'prod' + }); + } + } + } + + let packageManager: DependencyAnalysis['packageManager'] = 'pip'; + let lockFile: string | undefined; + + for (const lockFileName of PYTHON_PACKAGE_MANAGERS) { + if (fs.existsSync(path.join(directory, lockFileName))) { + lockFile = lockFileName; + if (lockFileName === 'poetry.lock') packageManager = 'poetry'; + break; + } + } + + return { dependencies, packageManager, lockFile }; +} + +async function analyzeGoDependencies( + directory: string +): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager'] }> { + const goModPath = path.join(directory, 'go.mod'); + + if (!fs.existsSync(goModPath)) { + return { dependencies: [], packageManager: 'go' }; + } + + const content = fs.readFileSync(goModPath, 'utf-8'); + const dependencies: DependencyInfo[] = []; + + const requireBlockMatch = content.match(/require\s*\(\s*([\s\S]*?)\s*\)/); + if (requireBlockMatch) { + const requireBlock = requireBlockMatch[1]; + const depRegex = /\b([^\s]+)\s+v?([^\s]+)/g; + let match; + while ((match = depRegex.exec(requireBlock)) !== null) { + const path = match[1]; + if (path && !path.startsWith('//') && path !== 'require') { + dependencies.push({ + name: path, + version: match[2] || 'latest', + type: 'prod' + }); + } + } + } + + return { dependencies, packageManager: 'go' }; +} + +async function analyzeRustDependencies( + directory: string +): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager'] }> { + const cargoTomlPath = path.join(directory, 'Cargo.toml'); + const cargoToml = parseTOMLFile(cargoTomlPath); + + if (!cargoToml) { + return { dependencies: [], packageManager: 'cargo' }; + } + + const dependencies: DependencyInfo[] = []; + + const deps = cargoToml.dependencies as Record | undefined; + if (deps) { + for (const [name, config] of Object.entries(deps)) { + const version = typeof config === 'string' ? config : (config as { version?: string }).version || '*'; + dependencies.push({ name, version, type: 'prod' }); + } + } + + const dev = cargoToml.dev as { dependencies?: Record } | undefined; + if (dev?.dependencies) { + for (const [name, config] of Object.entries(dev.dependencies)) { + const version = typeof config === 'string' ? config : (config as { version?: string }).version || '*'; + dependencies.push({ name, version, type: 'dev' }); + } + } + + return { dependencies, packageManager: 'cargo' }; +} + +async function analyzeJavaDependencies( + directory: string +): Promise<{ dependencies: DependencyInfo[]; packageManager: DependencyAnalysis['packageManager'] }> { + const dependencies: DependencyInfo[] = []; + + const pomPath = path.join(directory, 'pom.xml'); + if (fs.existsSync(pomPath)) { + const content = fs.readFileSync(pomPath, 'utf-8'); + + const dependencyRegex = /\s*([^<]+)<\/groupId>\s*([^<]+)<\/artifactId>\s*([^<]+)<\/version>\s*<\/dependency>/g; + let match; + + while ((match = dependencyRegex.exec(content)) !== null) { + dependencies.push({ + name: `${match[1]}:${match[2]}`, + version: match[3], + type: 'prod' + }); + } + + return { dependencies, packageManager: 'maven' }; + } + + const buildGradlePath = path.join(directory, 'build.gradle'); + const buildGradleKtsPath = path.join(directory, 'build.gradle.kts'); + + if (fs.existsSync(buildGradlePath) || fs.existsSync(buildGradleKtsPath)) { + const content = fs.readFileSync(buildGradlePath || buildGradleKtsPath, 'utf-8'); + + const dependencyRegex = /implementation\s+['"]([^:'"]+):([^:'"']+):([^'"@]+)@?(jar)?['"]/g; + let match; + + while ((match = dependencyRegex.exec(content)) !== null) { + dependencies.push({ + name: `${match[1]}:${match[2]}`, + version: match[3], + type: 'prod' + }); + } + + return { dependencies, packageManager: 'gradle' }; + } + + return { dependencies, packageManager: null }; +}