Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
IncrementalAnalysisService.ts12.5 kB
// Copyright 2025 Chris Bunting // Brief: Incremental analysis service for Static Analysis MCP Server // Scope: Provides intelligent caching and incremental analysis for large codebases import { readFileSync, existsSync, watchFile, unwatchFile, statSync, Stats } from 'fs'; import { join, dirname, basename, relative } from 'path'; import { watch } from 'fs'; import { AnalysisResult, AnalysisIssue, AnalysisMetrics, AnalysisOptions, SeverityLevel, IssueCategory, Language, FixSuggestion } from '@mcp-code-analysis/shared-types'; import { Logger } from '../utils/Logger.js'; import { StaticAnalysisService } from './StaticAnalysisService.js'; import { LanguageDetector } from './LanguageDetector.js'; export interface FileChange { path: string; type: 'created' | 'modified' | 'deleted'; timestamp: Date; size?: number; } export interface AnalysisCache { filePath: string; result: AnalysisResult; lastModified: Date; fileSize: number; checksum: string; } export interface IncrementalAnalysisOptions { enableCache: boolean; cacheTTL: number; // Time to live in milliseconds maxCacheSize: number; // Maximum number of files to cache watchMode: boolean; debounceTime: number; // Debounce time for file changes in milliseconds } export class IncrementalAnalysisService { private logger: Logger; private analysisService: StaticAnalysisService; private languageDetector: LanguageDetector; private cache: Map<string, AnalysisCache> = new Map(); private fileWatchers: Map<string, any> = new Map(); private pendingChanges: Map<string, FileChange> = new Map(); private debounceTimers: Map<string, NodeJS.Timeout> = new Map(); private options: IncrementalAnalysisOptions; constructor( analysisService: StaticAnalysisService, languageDetector: LanguageDetector, logger: Logger, options: Partial<IncrementalAnalysisOptions> = {} ) { this.analysisService = analysisService; this.languageDetector = languageDetector; this.logger = logger; this.options = { enableCache: true, cacheTTL: 3600000, // 1 hour maxCacheSize: 1000, watchMode: false, debounceTime: 1000, // 1 second ...options }; this.logger.info('Incremental Analysis Service initialized', this.options); } async analyzeFileIncremental( filePath: string, language?: string, options: AnalysisOptions = {} ): Promise<AnalysisResult> { try { this.logger.info(`Incremental analysis requested for: ${filePath}`); // Check if file exists if (!existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } // Get file stats const stats = statSync(filePath); const fileSize = stats.size; const lastModified = stats.mtime; // Check cache if enabled if (this.options.enableCache) { const cachedResult = this.getFromCache(filePath, fileSize, lastModified); if (cachedResult) { this.logger.info(`Returning cached result for: ${filePath}`); return cachedResult; } } // Perform full analysis const result = await this.analysisService.analyzeFile(filePath, language, options); // Cache the result if caching is enabled if (this.options.enableCache) { this.addToCache(filePath, result, fileSize, lastModified); } return result; } catch (error) { this.logger.error(`Error in incremental analysis for ${filePath}:`, error); throw error; } } async analyzeProjectIncremental( projectPath: string, filePatterns?: string[], excludePatterns?: string[], options: AnalysisOptions = {} ): Promise<AnalysisResult[]> { try { this.logger.info(`Incremental project analysis requested for: ${projectPath}`); // Get all files in the project const allFiles = await this.getProjectFiles(projectPath, filePatterns, excludePatterns); const results: AnalysisResult[] = []; // Analyze files incrementally for (const file of allFiles) { try { const result = await this.analyzeFileIncremental(file, undefined, options); results.push(result); } catch (error) { this.logger.warn(`Failed to incrementally analyze file ${file}:`, error); } } return results; } catch (error) { this.logger.error(`Error in incremental project analysis for ${projectPath}:`, error); throw error; } } async startWatching(projectPath: string): Promise<void> { try { if (!this.options.watchMode) { this.logger.info('Watch mode is disabled'); return; } this.logger.info(`Starting file watching for: ${projectPath}`); // Get all files in the project const files = await this.getProjectFiles(projectPath); // Start watching each file for (const file of files) { this.watchFile(file); } this.logger.info(`Started watching ${files.length} files`); } catch (error) { this.logger.error(`Error starting file watching for ${projectPath}:`, error); throw error; } } async stopWatching(): Promise<void> { try { this.logger.info('Stopping file watching'); // Clear all file watchers for (const [filePath, watcher] of this.fileWatchers) { watcher.close(); this.logger.debug(`Stopped watching: ${filePath}`); } this.fileWatchers.clear(); // Clear all debounce timers for (const [filePath, timer] of this.debounceTimers) { clearTimeout(timer); } this.debounceTimers.clear(); this.logger.info('File watching stopped'); } catch (error) { this.logger.error('Error stopping file watching:', error); throw error; } } async getChangedFiles(): Promise<FileChange[]> { return Array.from(this.pendingChanges.values()); } async clearPendingChanges(): Promise<void> { this.pendingChanges.clear(); } private async getProjectFiles( projectPath: string, includePatterns?: string[], excludePatterns?: string[] ): Promise<string[]> { const { glob } = await import('fast-glob'); const defaultPatterns = [ '**/*.{js,jsx,ts,tsx,py,java,c,cpp,go,rs}', '**/*.{h,hpp,cpp,cxx,cc}', ]; const defaultExcludePatterns = [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/target/**', '**/.git/**', '**/venv/**', '**/env/**', '**/__pycache__/**', '**/*.min.js', '**/*.bundle.js', ]; const patterns = includePatterns || defaultPatterns; const exclude = excludePatterns || defaultExcludePatterns; const files = await glob(patterns, { cwd: projectPath, ignore: exclude, absolute: true, onlyFiles: true, }); return files.sort(); } private watchFile(filePath: string): void { try { if (this.fileWatchers.has(filePath)) { this.logger.debug(`Already watching: ${filePath}`); return; } const watcher = watch(filePath, (eventType) => { if (eventType === 'change') { this.handleFileChange(filePath, statSync(filePath), statSync(filePath)); } }); this.fileWatchers.set(filePath, watcher); this.logger.debug(`Started watching: ${filePath}`); } catch (error) { this.logger.error(`Error watching file ${filePath}:`, error); } } private handleFileChange(filePath: string, curr: Stats, prev: Stats): void { try { // Clear existing debounce timer if (this.debounceTimers.has(filePath)) { clearTimeout(this.debounceTimers.get(filePath)!); } // Set new debounce timer const timer = setTimeout(() => { this.processFileChange(filePath, curr, prev); }, this.options.debounceTime); this.debounceTimers.set(filePath, timer); } catch (error) { this.logger.error(`Error handling file change for ${filePath}:`, error); } } private async processFileChange(filePath: string, curr: Stats, prev: Stats): Promise<void> { try { let changeType: FileChange['type']; if (!existsSync(filePath)) { changeType = 'deleted'; } else if (prev.size === 0 && curr.size > 0) { changeType = 'created'; } else { changeType = 'modified'; } const change: FileChange = { path: filePath, type: changeType, timestamp: new Date(), size: curr.size }; this.pendingChanges.set(filePath, change); this.logger.info(`File change detected: ${filePath} (${changeType})`); // Invalidate cache for this file if (changeType !== 'deleted') { this.cache.delete(filePath); } // If file was deleted, stop watching it if (changeType === 'deleted') { const watcher = this.fileWatchers.get(filePath); if (watcher) { watcher.close(); this.fileWatchers.delete(filePath); } } } catch (error) { this.logger.error(`Error processing file change for ${filePath}:`, error); } } private getFromCache( filePath: string, fileSize: number, lastModified: Date ): AnalysisResult | null { try { const cached = this.cache.get(filePath); if (!cached) { return null; } // Check if cache is expired const now = new Date(); const cacheAge = now.getTime() - cached.lastModified.getTime(); if (cacheAge > this.options.cacheTTL) { this.logger.debug(`Cache expired for: ${filePath}`); this.cache.delete(filePath); return null; } // Check if file has been modified if (cached.fileSize !== fileSize || cached.lastModified.getTime() !== lastModified.getTime()) { this.logger.debug(`File modified, cache invalid for: ${filePath}`); this.cache.delete(filePath); return null; } return cached.result; } catch (error) { this.logger.error(`Error getting from cache for ${filePath}:`, error); return null; } } private addToCache( filePath: string, result: AnalysisResult, fileSize: number, lastModified: Date ): void { try { // Check cache size limit if (this.cache.size >= this.options.maxCacheSize) { this.evictOldestCacheEntries(); } const checksum = this.generateChecksum(result); const cacheEntry: AnalysisCache = { filePath, result, lastModified, fileSize, checksum }; this.cache.set(filePath, cacheEntry); this.logger.debug(`Added to cache: ${filePath}`); } catch (error) { this.logger.error(`Error adding to cache for ${filePath}:`, error); } } private evictOldestCacheEntries(): void { try { // Sort cache entries by last modified time const entries = Array.from(this.cache.entries()) .sort((a, b) => a[1].lastModified.getTime() - b[1].lastModified.getTime()); // Remove oldest 25% of entries const entriesToRemove = Math.ceil(entries.length * 0.25); for (let i = 0; i < entriesToRemove; i++) { const [filePath] = entries[i]; this.cache.delete(filePath); this.logger.debug(`Evicted from cache: ${filePath}`); } } catch (error) { this.logger.error('Error evicting cache entries:', error); } } private generateChecksum(result: AnalysisResult): string { try { // Simple checksum based on issues count and severity const issuesData = result.issues.map(issue => `${issue.ruleId}:${issue.severity}:${issue.line}:${issue.column}` ).join('|'); return require('crypto') .createHash('md5') .update(issuesData) .digest('hex'); } catch (error) { this.logger.error('Error generating checksum:', error); return 'unknown'; } } getCacheStats(): any { return { size: this.cache.size, maxSize: this.options.maxCacheSize, ttl: this.options.cacheTTL, enabled: this.options.enableCache, watchMode: this.options.watchMode, watchedFiles: this.fileWatchers.size, pendingChanges: this.pendingChanges.size }; } clearCache(): void { this.cache.clear(); this.logger.info('Cache cleared'); } }

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/cbunting99/mcp-code-analysis-server'

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