UNPKG

@context-sync/server

Version:

MCP server for AI context sync with persistent memory, workspace file access, and intelligent code operations

504 lines 18.7 kB
import { promises as fsAsync } from 'fs'; import * as path from 'path'; export class TypeAnalyzer { workspacePath; fileCache; typeCache; // File size limits to prevent OOM crashes MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB - prevents OOM crashes WARN_FILE_SIZE = 1 * 1024 * 1024; // 1MB - warn but still process constructor(workspacePath) { this.workspacePath = workspacePath; this.fileCache = new Map(); this.typeCache = new Map(); } /** * Find type definition by name */ async findTypeDefinition(typeName) { const allFiles = await this.getAllProjectFiles(); for (const file of allFiles) { const types = await this.extractTypes(file); const found = types.find(t => t.name === typeName); if (found) return found; } return null; } /** * Get complete information about a type */ async getTypeInfo(typeName) { const definition = await this.findTypeDefinition(typeName); if (!definition) return null; const details = await this.getTypeDetails(definition); const usages = await this.findTypeUsages(typeName); const relatedTypes = await this.extractRelatedTypes(definition); return { definition, details, usages, relatedTypes }; } /** * Get detailed information based on type kind */ async getTypeDetails(definition) { const content = await this.readFile(definition.filePath); const lines = content.split('\n'); switch (definition.kind) { case 'interface': return await this.parseInterface(definition, lines); case 'type': return await this.parseTypeAlias(definition, lines); case 'class': return await this.parseClass(definition, lines); case 'enum': return await this.parseEnum(definition, lines); } } /** * Parse interface details */ async parseInterface(definition, lines) { const properties = []; const methods = []; let extendsTypes = []; // Find extends const headerLine = lines[definition.line - 1]; const extendsMatch = /extends\s+([\w\s,]+)/.exec(headerLine); if (extendsMatch) { extendsTypes = extendsMatch[1].split(',').map(t => t.trim()); } // Parse body let inInterface = false; let braceCount = 0; for (let i = definition.line - 1; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (trimmed.includes('interface')) { inInterface = true; } if (!inInterface) continue; braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; // Property: name: type or name?: type const propMatch = /^(readonly\s+)?(\w+)(\?)?:\s*([^;]+);?/.exec(trimmed); if (propMatch && !trimmed.includes('(')) { properties.push({ name: propMatch[2], type: propMatch[4].trim(), optional: !!propMatch[3], readonly: !!propMatch[1], line: i + 1 }); continue; } // Method: name(): returnType or name(params): returnType const methodMatch = /(\w+)\s*\(([^)]*)\)\s*:\s*([^;]+)/.exec(trimmed); if (methodMatch) { methods.push({ name: methodMatch[1], params: this.parseParameters(methodMatch[2]), returnType: methodMatch[3].trim(), isAsync: false, isStatic: false, line: i + 1 }); } if (braceCount === 0 && inInterface && i > definition.line - 1) { break; } } return { ...definition, kind: 'interface', properties, methods, extends: extendsTypes.length > 0 ? extendsTypes : undefined }; } /** * Parse type alias details */ async parseTypeAlias(definition, lines) { const line = lines[definition.line - 1]; const match = /type\s+\w+\s*=\s*(.+)/.exec(line); const typeDefinition = match ? match[1].trim() : ''; return { ...definition, kind: 'type', definition: typeDefinition }; } /** * Parse class details */ async parseClass(definition, lines) { const properties = []; const methods = []; let constructorInfo; let extendsClass; let implementsInterfaces = []; // Find extends and implements const headerLine = lines[definition.line - 1]; const extendsMatch = /extends\s+(\w+)/.exec(headerLine); if (extendsMatch) { extendsClass = extendsMatch[1]; } const implementsMatch = /implements\s+([\w\s,]+)/.exec(headerLine); if (implementsMatch) { implementsInterfaces = implementsMatch[1].split(',').map(t => t.trim()); } // Parse body let inClass = false; let braceCount = 0; for (let i = definition.line - 1; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (trimmed.includes('class')) { inClass = true; } if (!inClass) continue; braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; // Property: private/public/protected name: type const propMatch = /^(public|private|protected)?\s*(readonly\s+)?(\w+)(\?)?:\s*([^;=]+)/.exec(trimmed); if (propMatch && !trimmed.includes('(')) { properties.push({ name: propMatch[3], type: propMatch[5].trim(), optional: !!propMatch[4], readonly: !!propMatch[2], line: i + 1 }); continue; } // Constructor if (trimmed.includes('constructor')) { const constructorMatch = /constructor\s*\(([^)]*)\)/.exec(trimmed); if (constructorMatch) { constructorInfo = { name: 'constructor', params: this.parseParameters(constructorMatch[1]), isAsync: false, isStatic: false, line: i + 1 }; } continue; } // Method const methodMatch = /(public|private|protected)?\s*(static\s+)?(async\s+)?(\w+)\s*\(([^)]*)\)/.exec(trimmed); if (methodMatch && !trimmed.includes('if') && !trimmed.includes('while')) { const methodName = methodMatch[4]; if (methodName !== 'constructor') { methods.push({ name: methodName, params: this.parseParameters(methodMatch[5]), isAsync: !!methodMatch[3], isStatic: !!methodMatch[2], visibility: methodMatch[1] || 'public', line: i + 1 }); } } if (braceCount === 0 && inClass && i > definition.line - 1) { break; } } return { ...definition, kind: 'class', properties, methods, constructor: constructorInfo, extends: extendsClass, implements: implementsInterfaces.length > 0 ? implementsInterfaces : undefined }; } /** * Parse enum details */ async parseEnum(definition, lines) { const members = []; let inEnum = false; let braceCount = 0; for (let i = definition.line - 1; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (trimmed.includes('enum')) { inEnum = true; } if (!inEnum) continue; braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; // Enum member: NAME = value or NAME const memberMatch = /(\w+)\s*=?\s*([^,}]+)?/.exec(trimmed); if (memberMatch && !trimmed.includes('enum') && trimmed !== '}') { const value = memberMatch[2]?.trim().replace(/[,}]/g, ''); members.push({ name: memberMatch[1], value: value ? (isNaN(Number(value)) ? value : Number(value)) : undefined, line: i + 1 }); } if (braceCount === 0 && inEnum && i > definition.line - 1) { break; } } return { ...definition, kind: 'enum', members }; } /** * Find all usages of a type */ async findTypeUsages(typeName) { const usages = []; const allFiles = await this.getAllProjectFiles(); // Pre-compile regex patterns for better performance (fixes regex-in-loops) const escapedTypeName = typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const variableRegex = new RegExp(`\\w+\\s*:\\s*${escapedTypeName}`); const returnTypeRegex = new RegExp(`\\)\\s*:\\s*${escapedTypeName}`); for (const file of allFiles) { const content = await this.readFile(file); const lines = content.split('\n'); lines.forEach((line, lineNumber) => { const trimmed = line.trim(); // Skip the definition itself if (trimmed.includes(`interface ${typeName}`) || trimmed.includes(`type ${typeName}`) || trimmed.includes(`class ${typeName}`) || trimmed.includes(`enum ${typeName}`)) { return; } // Check for usage if (trimmed.includes(typeName)) { let usageType = 'variable'; if (trimmed.includes('implements') && trimmed.includes(typeName)) { usageType = 'implements'; } else if (trimmed.includes('extends') && trimmed.includes(typeName)) { usageType = 'extends'; } else if (trimmed.includes('<') && trimmed.includes(typeName)) { usageType = 'generic'; } else if (trimmed.match(variableRegex)) { usageType = 'variable'; } else if (trimmed.match(returnTypeRegex)) { usageType = 'return'; } usages.push({ filePath: file, line: lineNumber + 1, context: trimmed, usageType }); } }); } return usages; } /** * Extract all type definitions from a file */ async extractTypes(filePath) { if (this.typeCache.has(filePath)) { return this.typeCache.get(filePath); } const content = await this.readFile(filePath); const types = []; const lines = content.split('\n'); lines.forEach((line, lineNumber) => { const trimmed = line.trim(); // Interface const interfaceMatch = /(?:export\s+)?interface\s+(\w+)/.exec(trimmed); if (interfaceMatch) { types.push({ name: interfaceMatch[1], kind: 'interface', filePath, line: lineNumber + 1, isExported: trimmed.includes('export'), raw: line }); return; } // Type alias const typeMatch = /(?:export\s+)?type\s+(\w+)\s*=/.exec(trimmed); if (typeMatch) { types.push({ name: typeMatch[1], kind: 'type', filePath, line: lineNumber + 1, isExported: trimmed.includes('export'), raw: line }); return; } // Class const classMatch = /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/.exec(trimmed); if (classMatch) { types.push({ name: classMatch[1], kind: 'class', filePath, line: lineNumber + 1, isExported: trimmed.includes('export'), raw: line }); return; } // Enum const enumMatch = /(?:export\s+)?enum\s+(\w+)/.exec(trimmed); if (enumMatch) { types.push({ name: enumMatch[1], kind: 'enum', filePath, line: lineNumber + 1, isExported: trimmed.includes('export'), raw: line }); } }); this.typeCache.set(filePath, types); return types; } /** * Extract related types referenced in this type */ async extractRelatedTypes(definition) { const content = await this.readFile(definition.filePath); const lines = content.split('\n'); const relatedTypes = new Set(); // Get the type definition block let inType = false; let braceCount = 0; for (let i = definition.line - 1; i < lines.length; i++) { const line = lines[i]; if (i === definition.line - 1) { inType = true; } if (!inType) continue; braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; // Extract type references (capitalized words that might be types) const typeReferences = line.match(/:\s*([A-Z]\w+)/g); if (typeReferences) { typeReferences.forEach(ref => { const typeName = ref.replace(/:\s*/, ''); if (typeName !== definition.name) { relatedTypes.add(typeName); } }); } if (braceCount === 0 && inType && i > definition.line - 1) { break; } } return Array.from(relatedTypes); } /** * Parse function/method parameters */ parseParameters(paramString) { if (!paramString || !paramString.trim()) return []; return paramString.split(',').map(param => { const trimmed = param.trim(); const optional = trimmed.includes('?'); const hasDefault = trimmed.includes('='); // Extract name, type, and default value const match = /(\w+)(\?)?:\s*([^=]+)(?:=\s*(.+))?/.exec(trimmed); if (match) { return { name: match[1], type: match[3]?.trim(), optional: optional || hasDefault, defaultValue: match[4]?.trim() }; } // Simple parameter without type const simpleMatch = /(\w+)(\?)?/.exec(trimmed); return { name: simpleMatch?.[1] || trimmed, optional: optional, }; }); } // Helper methods async readFile(filePath) { if (this.fileCache.has(filePath)) { return this.fileCache.get(filePath); } try { // Check file size first to prevent OOM crashes const stats = await fsAsync.stat(filePath); if (stats.size > this.MAX_FILE_SIZE) { console.error(`⚠️ File too large for type analysis (${(stats.size / 1024 / 1024).toFixed(1)}MB), skipping: ${this.getRelativePath(filePath)}`); return ''; } if (stats.size > this.WARN_FILE_SIZE) { console.error(`⚠️ Large file in type analysis (${(stats.size / 1024 / 1024).toFixed(1)}MB): ${this.getRelativePath(filePath)}`); } const content = await fsAsync.readFile(filePath, 'utf-8'); this.fileCache.set(filePath, content); return content; } catch (error) { return ''; } } getRelativePath(filePath) { return path.relative(this.workspacePath, filePath); } async getAllProjectFiles() { const files = []; const extensions = ['.ts', '.tsx']; // Only TypeScript files for type analysis const walk = async (dir) => { try { const entries = await fsAsync.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!['node_modules', 'dist', 'build', '.git', '.next', 'out', 'coverage'].includes(entry.name)) { await walk(fullPath); } } else { const ext = path.extname(entry.name); if (extensions.includes(ext)) { files.push(fullPath); } } } } catch (error) { // Skip directories we can't read } }; await walk(this.workspacePath); return files; } /** * Clear caches */ clearCache() { this.fileCache.clear(); this.typeCache.clear(); } } //# sourceMappingURL=type-analyzer.js.map