@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
JavaScript
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