From b6267050d54c18dda2761ed07d309ac5bb35a881 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sun, 1 Feb 2026 01:43:57 +0000 Subject: [PATCH] fix: resolve CI test failures --- .../src/analyzers/dependencyAnalyzer.ts | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 .ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts diff --git a/.ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts b/.ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts new file mode 100644 index 0000000..dec61c7 --- /dev/null +++ b/.ai-context-generator-cli/src/analyzers/dependencyAnalyzer.ts @@ -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 { + 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 | 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 | 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 { + try { + await fs.promises.access(filePath, fs.constants.F_OK); + return true; + } catch { + return false; + } + } +}