Skip to main content
Glama
FosterG4

Code Reference Optimizer MCP Server

by FosterG4
ImportAnalyzer.ts16.4 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import { parse as babelParse } from '@babel/parser'; import traverse from '@babel/traverse'; import * as t from '@babel/types'; import type { ImportStatement, DependencyGraph } from '../types/index.js'; export class ImportAnalyzer { private importCache: Map<string, ImportStatement[]> = new Map(); private dependencyGraph: Map<string, DependencyGraph> = new Map(); private packageJsonCache: Map<string, any> = new Map(); /** * Extract all import statements from a file */ async extractImports(filePath: string): Promise<string[]> { const cached = this.importCache.get(filePath); if (cached) { return cached.map(imp => imp.raw); } const content = await fs.readFile(filePath, 'utf-8'); const imports = await this.parseImports(content, filePath); this.importCache.set(filePath, imports); return imports.map(imp => imp.raw); } /** * Get minimal imports needed for specific symbols */ async getMinimalImports(filePath: string, usedSymbols: string[]): Promise<string[]> { await this.extractImports(filePath); const importStatements = this.importCache.get(filePath) || []; if (usedSymbols.length === 0) { return []; } const minimalImports: string[] = []; const usedSymbolSet = new Set(usedSymbols); for (const importStmt of importStatements) { const relevantImports = this.filterRelevantImports(importStmt, usedSymbolSet); if (relevantImports) { minimalImports.push(relevantImports); } } // Add transitive dependencies const transitiveDeps = await this.getTransitiveDependencies(filePath, usedSymbols); for (const dep of transitiveDeps) { if (!minimalImports.some(imp => imp.includes(dep))) { minimalImports.push(`import '${dep}';`); } } return this.deduplicateImports(minimalImports); } /** * Analyze dependency relationships between files */ async analyzeDependencies(rootPath: string): Promise<DependencyGraph> { const cached = this.dependencyGraph.get(rootPath); if (cached) { return cached; } const graph: DependencyGraph = { nodes: new Map(), edges: new Map(), }; await this.buildDependencyGraph(rootPath, graph); this.dependencyGraph.set(rootPath, graph); return graph; } /** * Optimize imports by removing unused dependencies */ async optimizeImports(filePath: string, actuallyUsedSymbols: string[]): Promise<{ optimizedImports: string[]; removedImports: string[]; addedImports: string[]; warnings: string[]; }> { const originalImports = await this.extractImports(filePath); const minimalImports = await this.getMinimalImports(filePath, actuallyUsedSymbols); const removedImports = originalImports.filter(imp => !minimalImports.includes(imp)); const addedImports = minimalImports.filter(imp => !originalImports.includes(imp)); const warnings: string[] = []; // Check for potential issues const sideEffectImports = this.detectSideEffectImports(originalImports); for (const sideEffect of sideEffectImports) { if (removedImports.includes(sideEffect)) { warnings.push(`Removed side-effect import: ${sideEffect}`); } } // Check for circular dependencies const circularDeps = await this.detectCircularDependencies(filePath); if (circularDeps.length > 0) { warnings.push(`Circular dependencies detected: ${circularDeps.join(', ')}`); } return { optimizedImports: minimalImports, removedImports, addedImports, warnings, }; } /** * Get import suggestions based on usage patterns */ async getImportSuggestions(filePath: string, undefinedSymbols: string[]): Promise<Array<{ symbol: string; suggestions: Array<{ importStatement: string; confidence: number; source: string; }>; }>> { const suggestions: Array<{ symbol: string; suggestions: Array<{ importStatement: string; confidence: number; source: string; }>; }> = []; for (const symbol of undefinedSymbols) { const symbolSuggestions = await this.findImportSuggestions(symbol, filePath); suggestions.push({ symbol, suggestions: symbolSuggestions, }); } return suggestions; } /** * Clear caches */ clearCache(): void { this.importCache.clear(); this.dependencyGraph.clear(); this.packageJsonCache.clear(); } private async parseImports(content: string, filePath: string): Promise<ImportStatement[]> { const imports: ImportStatement[] = []; try { const ast = babelParse(content, { sourceType: 'module', allowImportExportEverywhere: true, plugins: [ 'jsx', 'typescript', 'decorators-legacy', 'classProperties', 'objectRestSpread', 'asyncGenerators', 'functionBind', 'exportDefaultFrom', 'exportNamespaceFrom', 'dynamicImport', 'nullishCoalescingOperator', 'optionalChaining', ], }); const self = this; traverse(ast, { ImportDeclaration(path) { const node = path.node; const importStatement = self.parseImportDeclaration(node, content); if (importStatement) { imports.push(importStatement); } }, CallExpression(path) { // Handle dynamic imports const node = path.node; if (t.isImport(node.callee) && t.isStringLiteral(node.arguments[0])) { imports.push({ source: node.arguments[0].value, imports: [], raw: `import('${node.arguments[0].value}')`, }); } }, }); } catch (error) { console.warn(`Failed to parse imports from ${filePath}:`, error); } return imports; } private parseImportDeclaration(node: t.ImportDeclaration, content: string): ImportStatement | null { const source = node.source.value; const imports: ImportStatement['imports'] = []; for (const specifier of node.specifiers) { if (t.isImportDefaultSpecifier(specifier)) { imports.push({ name: specifier.local.name, isDefault: true, }); } else if (t.isImportNamespaceSpecifier(specifier)) { imports.push({ name: specifier.local.name, isNamespace: true, }); } else if (t.isImportSpecifier(specifier)) { const importedName = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value; const alias = specifier.local.name !== importedName ? specifier.local.name : undefined; imports.push({ name: importedName, ...(alias && { alias }), }); } } // Extract raw import statement from source const start = node.start || 0; const end = node.end || 0; const raw = content.slice(start, end); return { source, imports, raw, }; } private filterRelevantImports(importStmt: ImportStatement, usedSymbols: Set<string>): string | null { const relevantImports = importStmt.imports.filter(imp => { const symbolName = imp.alias || imp.name; return usedSymbols.has(symbolName); }); if (relevantImports.length === 0) { // Check if it's a side-effect import if (importStmt.imports.length === 0) { return importStmt.raw; // Keep side-effect imports } return null; } // Reconstruct import statement with only relevant imports return this.reconstructImportStatement(importStmt.source, relevantImports); } private reconstructImportStatement(source: string, imports: ImportStatement['imports']): string { if (imports.length === 0) { return `import '${source}';`; } const parts: string[] = []; const namedImports: string[] = []; for (const imp of imports) { if (imp.isDefault) { parts.push(imp.name); } else if (imp.isNamespace) { parts.push(`* as ${imp.name}`); } else { const importName = imp.alias ? `${imp.name} as ${imp.alias}` : imp.name; namedImports.push(importName); } } if (namedImports.length > 0) { parts.push(`{ ${namedImports.join(', ')} }`); } return `import ${parts.join(', ')} from '${source}';`; } private async getTransitiveDependencies(filePath: string, symbols: string[]): Promise<string[]> { const dependencies: Set<string> = new Set(); const visited: Set<string> = new Set(); const traverse = async (currentFile: string, currentSymbols: string[]) => { if (visited.has(currentFile)) { return; } visited.add(currentFile); try { await this.extractImports(currentFile); const importStatements = this.importCache.get(currentFile) || []; for (const importStmt of importStatements) { const resolvedPath = await this.resolveImportPath(importStmt.source, currentFile); if (resolvedPath && !this.isNodeModule(importStmt.source)) { dependencies.add(importStmt.source); // Check if any of the imported symbols are used const usedFromThisImport = importStmt.imports.filter(imp => currentSymbols.includes(imp.alias || imp.name) ); if (usedFromThisImport.length > 0) { await traverse(resolvedPath, usedFromThisImport.map(imp => imp.name)); } } } } catch (error) { // Ignore errors for files that can't be read } }; await traverse(filePath, symbols); return Array.from(dependencies); } private async buildDependencyGraph(rootPath: string, graph: DependencyGraph): Promise<void> { const visited: Set<string> = new Set(); const traverse = async (filePath: string) => { if (visited.has(filePath)) { return; } visited.add(filePath); try { await this.extractImports(filePath); const importStatements = this.importCache.get(filePath) || []; if (!graph.edges.has(filePath)) { graph.edges.set(filePath, new Set()); } for (const importStmt of importStatements) { const resolvedPath = await this.resolveImportPath(importStmt.source, filePath); if (resolvedPath && !this.isNodeModule(importStmt.source)) { graph.edges.get(filePath)?.add(resolvedPath); await traverse(resolvedPath); } } } catch (error) { // Ignore errors for files that can't be read } }; await traverse(rootPath); } private deduplicateImports(imports: string[]): string[] { const seen = new Set<string>(); const deduplicated: string[] = []; for (const imp of imports) { const normalized = imp.trim(); if (!seen.has(normalized)) { seen.add(normalized); deduplicated.push(imp); } } return deduplicated; } private detectSideEffectImports(imports: string[]): string[] { return imports.filter(imp => { // Side-effect imports typically don't have named imports return imp.match(/^import\s+['"][^'"]+['"];?$/); }); } private async detectCircularDependencies(filePath: string): Promise<string[]> { const graph = await this.analyzeDependencies(filePath); const cycles: string[] = []; const visited = new Set<string>(); const recursionStack = new Set<string>(); const dfs = (node: string, path: string[]): void => { if (recursionStack.has(node)) { const cycleStart = path.indexOf(node); cycles.push(path.slice(cycleStart).join(' -> ')); return; } if (visited.has(node)) { return; } visited.add(node); recursionStack.add(node); const neighbors = graph.edges.get(node) || new Set(); for (const neighbor of neighbors) { dfs(neighbor, [...path, neighbor]); } recursionStack.delete(node); }; dfs(filePath, [filePath]); return cycles; } private async findImportSuggestions(symbol: string, filePath: string): Promise<Array<{ importStatement: string; confidence: number; source: string; }>> { const suggestions: Array<{ importStatement: string; confidence: number; source: string; }> = []; // Check package.json dependencies const packageJson = await this.getPackageJson(filePath); if (packageJson) { const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.peerDependencies, }; for (const [pkg, _version] of Object.entries(allDeps)) { // Simple heuristic: if symbol name matches or is similar to package name const similarity = this.calculateSimilarity(symbol.toLowerCase(), pkg.toLowerCase()); if (similarity > 0.6) { suggestions.push({ importStatement: `import { ${symbol} } from '${pkg}';`, confidence: similarity, source: pkg, }); } } } return suggestions.sort((a, b) => b.confidence - a.confidence); } private async resolveImportPath(importPath: string, fromFile: string): Promise<string | null> { if (this.isNodeModule(importPath)) { return null; // Don't resolve node modules } const dir = path.dirname(fromFile); const resolved = path.resolve(dir, importPath); // Try different extensions const extensions = ['.ts', '.tsx', '.js', '.jsx', '.json']; for (const ext of extensions) { const withExt = resolved + ext; try { await fs.access(withExt); return withExt; } catch { // Continue to next extension } } // Try index files for (const ext of extensions) { const indexFile = path.join(resolved, `index${ext}`); try { await fs.access(indexFile); return indexFile; } catch { // Continue to next extension } } return null; } private isNodeModule(importPath: string): boolean { return !importPath.startsWith('.') && !importPath.startsWith('/'); } private async getPackageJson(filePath: string): Promise<any | null> { let dir = path.dirname(filePath); while (dir !== path.dirname(dir)) { const packageJsonPath = path.join(dir, 'package.json'); if (this.packageJsonCache.has(packageJsonPath)) { return this.packageJsonCache.get(packageJsonPath); } try { const content = await fs.readFile(packageJsonPath, 'utf-8'); const packageJson = JSON.parse(content); this.packageJsonCache.set(packageJsonPath, packageJson); return packageJson; } catch { // Continue to parent directory } dir = path.dirname(dir); } return null; } private calculateSimilarity(str1: string, str2: string): number { const longer = str1.length > str2.length ? str1 : str2; const shorter = str1.length > str2.length ? str2 : str1; if (longer.length === 0) { return 1.0; } const editDistance = this.levenshteinDistance(longer, shorter); return (longer.length - editDistance) / longer.length; } private levenshteinDistance(str1: string, str2: string): number { const matrix: number[][] = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(0)); for (let i = 0; i <= str1.length; i++) { matrix[0]![i] = i; } for (let j = 0; j <= str2.length; j++) { matrix[j]![0] = j; } for (let j = 1; j <= str2.length; j++) { for (let i = 1; i <= str1.length; i++) { const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j]![i] = Math.min( matrix[j]![i - 1]! + 1, // deletion matrix[j - 1]![i]! + 1, // insertion matrix[j - 1]![i - 1]! + indicator // substitution ); } } return matrix[str2.length]![str1.length]!; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/FosterG4/mcpsaver'

If you have feedback or need assistance with the MCP directory API, please join our Discord server