This commit is contained in:
263
app/src/analyzers/dependencyAnalyzer.ts
Normal file
263
app/src/analyzers/dependencyAnalyzer.ts
Normal file
@@ -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<DependencyAnalysis> {
|
||||
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<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
}>(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<string, unknown> | 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<string, string | { version?: string }> | 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<string, string | { version?: string }> } | 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 = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>\s*<version>([^<]+)<\/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 };
|
||||
}
|
||||
Reference in New Issue
Block a user