This commit is contained in:
375
.ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts
Normal file
375
.ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DependencyInfo, Dependency } from '../types';
|
||||||
|
|
||||||
|
interface DependencyFile {
|
||||||
|
path: string;
|
||||||
|
parser: (content: string) => Dependency[];
|
||||||
|
type: 'prod' | 'dev' | 'both';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DependencyAnalyzer {
|
||||||
|
private dependencyFiles: DependencyFile[] = [
|
||||||
|
{
|
||||||
|
path: 'package.json',
|
||||||
|
parser: this.parsePackageJson.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'requirements.txt',
|
||||||
|
parser: this.parseRequirementsTxt.bind(this),
|
||||||
|
type: 'prod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pyproject.toml',
|
||||||
|
parser: this.parsePyprojectToml.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'go.mod',
|
||||||
|
parser: this.parseGoMod.bind(this),
|
||||||
|
type: 'prod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Cargo.toml',
|
||||||
|
parser: this.parseCargoToml.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Pipfile',
|
||||||
|
parser: this.parsePipfile.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'Gemfile',
|
||||||
|
parser: this.parseGemfile.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'composer.json',
|
||||||
|
parser: this.parseComposerJson.bind(this),
|
||||||
|
type: 'both',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async analyze(
|
||||||
|
dir: string,
|
||||||
|
includeDev: boolean = false
|
||||||
|
): Promise<DependencyInfo> {
|
||||||
|
const allDependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
for (const depFile of this.dependencyFiles) {
|
||||||
|
const filePath = path.join(dir, depFile.path);
|
||||||
|
if (await this.fileExists(filePath)) {
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||||
|
const deps = depFile.parser(content);
|
||||||
|
|
||||||
|
if (depFile.type === 'both') {
|
||||||
|
const filteredDeps = includeDev
|
||||||
|
? deps
|
||||||
|
: deps.filter(d => d.type === 'prod');
|
||||||
|
allDependencies.push(...filteredDeps);
|
||||||
|
} else if (depFile.type === 'prod' || includeDev) {
|
||||||
|
allDependencies.push(...deps);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to parse ${depFile.path}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = allDependencies.filter(d => d.type === 'prod');
|
||||||
|
const dev = allDependencies.filter(d => d.type === 'dev');
|
||||||
|
|
||||||
|
return {
|
||||||
|
direct,
|
||||||
|
dev,
|
||||||
|
total: direct.length + dev.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePackageJson(content: string): Dependency[] {
|
||||||
|
const packageJson = JSON.parse(content);
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
const parseDeps = (
|
||||||
|
deps: Record<string, string> | undefined,
|
||||||
|
type: 'prod' | 'dev'
|
||||||
|
) => {
|
||||||
|
if (!deps) return;
|
||||||
|
for (const [name, version] of Object.entries(deps)) {
|
||||||
|
dependencies.push({
|
||||||
|
name,
|
||||||
|
version: version as string,
|
||||||
|
type,
|
||||||
|
isLocal: name.startsWith('.') || name.startsWith('/') || name.startsWith('@'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
parseDeps(packageJson.dependencies, 'prod');
|
||||||
|
parseDeps(packageJson.devDependencies, 'dev');
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseRequirementsTxt(content: string): Dependency[] {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] + match[3],
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
||||||
|
dependencies.push({
|
||||||
|
name: trimmed,
|
||||||
|
version: '*',
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePyprojectToml(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let inDependencies = false;
|
||||||
|
let inDevDependencies = false;
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'project') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
inDependencies = true;
|
||||||
|
inDevDependencies = false;
|
||||||
|
} else if (section === 'project.optional-dependencies') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
inDevDependencies = true;
|
||||||
|
inDependencies = false;
|
||||||
|
} else if (section === 'dependencies' || section === 'tool.poetry.dependencies') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
inDependencies = true;
|
||||||
|
inDevDependencies = false;
|
||||||
|
} else if (section === 'dev-dependencies' || section === 'tool.poetry.dev-dependencies') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
inDevDependencies = true;
|
||||||
|
inDependencies = false;
|
||||||
|
} else {
|
||||||
|
inDependencies = false;
|
||||||
|
inDevDependencies = false;
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inDependencies && !inDevDependencies) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const cleanLine = trimmed.replace(/^["']|["',]/g, '');
|
||||||
|
const match = cleanLine.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] + match[3],
|
||||||
|
type: currentSection || 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else if (/^[a-zA-Z0-9_-]+$/.test(cleanLine)) {
|
||||||
|
dependencies.push({
|
||||||
|
name: trimmed,
|
||||||
|
version: '*',
|
||||||
|
type: currentSection || 'prod',
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseGoMod(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('module') || trimmed.startsWith('go')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9./_-]+)\s+v?([0-9.]+)/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2],
|
||||||
|
type: 'prod',
|
||||||
|
isLocal: match[1].startsWith('./') || match[1].startsWith('../'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCargoToml(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'dependencies') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
} else if (section === 'dev-dependencies') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
} else {
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSection) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*["\']?(.+?)["\']?\s*,?$/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2].replace(/["']/g, ''),
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePipfile(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||||
|
const section = trimmed.slice(1, -1).toLowerCase();
|
||||||
|
if (section === 'packages') {
|
||||||
|
currentSection = 'prod';
|
||||||
|
} else if (section === 'dev-packages') {
|
||||||
|
currentSection = 'dev';
|
||||||
|
} else {
|
||||||
|
currentSection = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSection) continue;
|
||||||
|
if (trimmed.startsWith('#') || !trimmed) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*\{?\s*version\s*=\s*["']([^"']+)["']/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2],
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const simpleMatch = trimmed.match(/^([a-zA-Z0-9_-]+)/);
|
||||||
|
if (simpleMatch) {
|
||||||
|
dependencies.push({
|
||||||
|
name: simpleMatch[1],
|
||||||
|
version: '*',
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseGemfile(content: string): Dependency[] {
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentSection: 'prod' | 'dev' = 'prod';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
|
||||||
|
if (trimmed.startsWith('group') && trimmed.includes(':development') || trimmed.includes(':test')) {
|
||||||
|
currentSection = 'dev';
|
||||||
|
continue;
|
||||||
|
} else if (trimmed.startsWith('group') || trimmed.startsWith('source')) {
|
||||||
|
currentSection = 'prod';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = trimmed.match(/^gem\s+["']([^"']+)["'](?:\s*,\s*version:\s*["']([^"']+)["'])?/);
|
||||||
|
if (match) {
|
||||||
|
dependencies.push({
|
||||||
|
name: match[1],
|
||||||
|
version: match[2] || '*',
|
||||||
|
type: currentSection,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseComposerJson(content: string): Dependency[] {
|
||||||
|
const composerJson = JSON.parse(content);
|
||||||
|
const dependencies: Dependency[] = [];
|
||||||
|
|
||||||
|
const parseDeps = (deps: Record<string, string> | undefined, type: 'prod' | 'dev') => {
|
||||||
|
if (!deps) return;
|
||||||
|
for (const [name, version] of Object.entries(deps)) {
|
||||||
|
dependencies.push({
|
||||||
|
name,
|
||||||
|
version: version as string,
|
||||||
|
type,
|
||||||
|
isLocal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
parseDeps(composerJson.require, 'prod');
|
||||||
|
parseDeps(composerJson['require-dev'], 'dev');
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user