Skip to main content
Glama

NervusDB MCP Server

Official
by nervusdb
refactoringSuggester.ts16.8 kB
/** * Refactoring Suggester Service * * Generates intelligent refactoring suggestions based on detected code smells. * Provides priority scoring, impact analysis, and actionable recommendations. * * Supported Refactoring Patterns: * 1. Extract Method (for God Functions and Long Functions) * 2. Introduce Parameter Object (for Long Parameter Lists) * 3. Remove Dead Code (for Dead Code smells) * 4. Inline Function (for trivial wrappers) * 5. Rename Symbol (for poor naming) * 6. Split Function (for functions doing too much) */ import type { QueryService, CodeEntityInfo, CallHierarchyNode, } from '../domain/query/queryService.js'; import type { CodeSmell } from './codeSmellDetector.js'; /** * Refactoring pattern types */ export type RefactoringPattern = | 'extract-method' | 'introduce-parameter-object' | 'remove-dead-code' | 'inline-function' | 'rename-symbol' | 'split-function'; /** * Priority level for refactoring */ export type RefactoringPriority = 'high' | 'medium' | 'low'; /** * A specific refactoring suggestion */ export interface RefactoringSuggestion { id: string; pattern: RefactoringPattern; priority: RefactoringPriority; targetEntity: CodeEntityInfo; smell?: CodeSmell; // The smell that triggered this suggestion title: string; description: string; reasoning: string; benefits: string[]; risks: string[]; score: { benefit: number; // 0-10 risk: number; // 0-10 effort: number; // 0-10 (higher = more work) impact: number; // 0-10 (higher = more files affected) overall: number; // Weighted score for sorting }; steps: string[]; // Step-by-step instructions estimatedTime?: string; // e.g., "15 minutes" affectedFiles?: string[]; } /** * Input for refactoring suggestion */ export interface SuggestRefactoringsInput { projectPath: string; smells?: CodeSmell[]; // Optional: provide detected smells targetSymbol?: string; // Optional: focus on specific symbol maxSuggestions?: number; // Limit number of suggestions } /** * Result of refactoring suggestion */ export interface SuggestRefactoringsResult { projectPath: string; suggestions: RefactoringSuggestion[]; summary: { totalSuggestions: number; byPattern: Record<RefactoringPattern, number>; byPriority: Record<RefactoringPriority, number>; }; stats: { analysisTimeMs: number; }; } /** * Service for generating refactoring suggestions */ export class RefactoringSuggester { constructor(private readonly deps: { queryService: QueryService }) {} /** * Generate refactoring suggestions based on code smells */ async suggestRefactorings(input: SuggestRefactoringsInput): Promise<SuggestRefactoringsResult> { const startTime = Date.now(); const suggestions: RefactoringSuggestion[] = []; // Process each smell and generate suggestions if (input.smells && input.smells.length > 0) { for (const smell of input.smells) { const smellSuggestions = await this.generateSuggestionsForSmell(input.projectPath, smell); suggestions.push(...smellSuggestions); } } // Sort by overall score (descending) suggestions.sort((a, b) => b.score.overall - a.score.overall); // Limit if requested const limitedSuggestions = input.maxSuggestions ? suggestions.slice(0, input.maxSuggestions) : suggestions; // Generate summary const summary = this.generateSummary(limitedSuggestions); return { projectPath: input.projectPath, suggestions: limitedSuggestions, summary, stats: { analysisTimeMs: Date.now() - startTime, }, }; } /** * Generate suggestions for a specific code smell */ private async generateSuggestionsForSmell( projectPath: string, smell: CodeSmell, ): Promise<RefactoringSuggestion[]> { const suggestions: RefactoringSuggestion[] = []; switch (smell.type) { case 'god-function': suggestions.push(...(await this.suggestExtractMethod(projectPath, smell))); break; case 'long-function': suggestions.push(...(await this.suggestSplitFunction(projectPath, smell))); break; case 'long-parameters': suggestions.push(await this.suggestParameterObject(projectPath, smell)); break; case 'dead-code': suggestions.push(await this.suggestRemoveDeadCode(projectPath, smell)); break; default: // Other smell types not yet supported break; } return suggestions; } /** * Suggest Extract Method refactoring for God Function */ private async suggestExtractMethod( projectPath: string, smell: CodeSmell, ): Promise<RefactoringSuggestion[]> { const suggestions: RefactoringSuggestion[] = []; try { // Get call hierarchy to see what this function calls const hierarchy = await this.deps.queryService.getCallHierarchy( projectPath, smell.entity.name, 1, // depth 1 to see direct callees ); const callees = hierarchy.callees; if (callees.length === 0) { return suggestions; } // Group callees by logical concerns (heuristic: similar names) const groups = this.groupCalleesByLogic(callees); // Generate suggestion for each group let suggestionCount = 0; for (const group of groups) { if (group.callees.length < 2) continue; // Need at least 2 to extract suggestionCount++; const groupName = group.name; const suggestion: RefactoringSuggestion = { id: `extract-method-${smell.entity.name}-${suggestionCount}`, pattern: 'extract-method', priority: this.calculatePriority(smell, callees.length), targetEntity: smell.entity, smell, title: `Extract "${groupName}" logic into separate method`, description: `Extract ${group.callees.length} related function calls into a dedicated method`, reasoning: `This function has high complexity (${smell.metrics?.complexity}). Extracting ${groupName} logic will improve readability and testability.`, benefits: [ 'Reduces function complexity', 'Improves code readability', 'Makes logic reusable', 'Easier to test in isolation', ], risks: ['May introduce new parameters', 'Need to ensure correct scope'], score: { benefit: Math.min(10, Math.floor((smell.metrics?.complexity || 10) / 2)), risk: 2, // Low risk for extract method effort: Math.min(10, Math.floor(group.callees.length / 2)), impact: await this.calculateImpact(projectPath, smell.entity.name), overall: 0, // Will be calculated }, steps: [ `1. Identify the ${groupName} code block in ${smell.entity.name}`, `2. Create a new private method: extract${this.capitalize(groupName)}()`, '3. Move the identified code into the new method', '4. Replace original code with a call to the new method', '5. Run tests to verify behavior unchanged', ], estimatedTime: this.estimateTime(group.callees.length), affectedFiles: [smell.location.file], }; // Calculate overall score suggestion.score.overall = this.calculateOverallScore(suggestion.score); suggestions.push(suggestion); } } catch (error) { // Failed to get hierarchy, skip } return suggestions; } /** * Suggest Split Function for Long Function */ private async suggestSplitFunction( projectPath: string, smell: CodeSmell, ): Promise<RefactoringSuggestion[]> { const lineCount = smell.metrics?.lineCount || 0; const suggestion: RefactoringSuggestion = { id: `split-function-${smell.entity.name}`, pattern: 'split-function', priority: this.calculatePriority(smell, lineCount), targetEntity: smell.entity, smell, title: `Split ${smell.entity.name} into smaller functions`, description: `This function has ${lineCount} lines. Split into logical units.`, reasoning: `Long functions are hard to understand and maintain. Breaking it down will improve clarity.`, benefits: [ 'Improves readability', 'Easier to test', 'Better separation of concerns', 'Reduces cognitive load', ], risks: ['Need to identify logical boundaries', 'May require passing more parameters'], score: { benefit: Math.min(10, Math.floor(lineCount / 10)), risk: 3, effort: Math.min(10, Math.floor(lineCount / 15)), impact: await this.calculateImpact(projectPath, smell.entity.name), overall: 0, }, steps: [ '1. Read through the function to identify logical sections', '2. Look for sequential blocks that accomplish distinct tasks', '3. For each block, create a descriptive private method', '4. Extract the code into the new method', '5. Replace with method call', '6. Run tests after each extraction', ], estimatedTime: this.estimateTime(Math.floor(lineCount / 20)), affectedFiles: [smell.location.file], }; suggestion.score.overall = this.calculateOverallScore(suggestion.score); return [suggestion]; } /** * Suggest Introduce Parameter Object for Long Parameter List */ private async suggestParameterObject( projectPath: string, smell: CodeSmell, ): Promise<RefactoringSuggestion> { const paramCount = smell.metrics?.parameterCount || 0; const suggestion: RefactoringSuggestion = { id: `param-object-${smell.entity.name}`, pattern: 'introduce-parameter-object', priority: this.calculatePriority(smell, paramCount), targetEntity: smell.entity, smell, title: `Introduce parameter object for ${smell.entity.name}`, description: `Replace ${paramCount} parameters with a configuration object`, reasoning: `Functions with many parameters are hard to call correctly. A parameter object simplifies the interface.`, benefits: [ 'Simplifies function signature', 'Makes parameters self-documenting', 'Easy to add new options later', 'Reduces chance of argument order errors', ], risks: ['All callers need to be updated', 'Need to define the parameter interface'], score: { benefit: Math.min(10, paramCount), risk: await this.calculateImpact(projectPath, smell.entity.name), effort: Math.min(10, Math.floor(paramCount / 2)), impact: await this.calculateImpact(projectPath, smell.entity.name), overall: 0, }, steps: [ `1. Create a new interface: ${smell.entity.name}Options`, '2. Define properties for each parameter', '3. Update function signature to accept the options object', '4. Update function body to use options.propertyName', '5. Find all callers and update them', '6. Run tests to verify', ], estimatedTime: this.estimateTime(paramCount), affectedFiles: [smell.location.file], }; suggestion.score.overall = this.calculateOverallScore(suggestion.score); return suggestion; } /** * Suggest Remove Dead Code */ private async suggestRemoveDeadCode( projectPath: string, smell: CodeSmell, ): Promise<RefactoringSuggestion> { const suggestion: RefactoringSuggestion = { id: `remove-dead-${smell.entity.name}`, pattern: 'remove-dead-code', priority: 'low', // Dead code is low priority targetEntity: smell.entity, smell, title: `Remove unused function ${smell.entity.name}`, description: 'This function has no callers and can be safely removed', reasoning: 'Dead code increases maintenance burden and confuses developers.', benefits: ['Reduces codebase size', 'Improves code clarity', 'Less code to maintain'], risks: ['May be called dynamically (reflection)', 'May be part of public API'], score: { benefit: 5, // Moderate benefit risk: 2, // Low risk (but check if exported) effort: 1, // Very easy to remove impact: 1, // No impact (no callers) overall: 0, }, steps: [ '1. Verify function is not exported as public API', '2. Search for dynamic calls (string references)', '3. Delete the function', '4. Run tests to ensure nothing breaks', ], estimatedTime: '5 minutes', affectedFiles: [smell.location.file], }; suggestion.score.overall = this.calculateOverallScore(suggestion.score); return suggestion; } /** * Group callees by logical concern (heuristic based on naming) */ private groupCalleesByLogic( callees: CallHierarchyNode[], ): Array<{ name: string; callees: CallHierarchyNode[] }> { const groups = new Map<string, CallHierarchyNode[]>(); // Simple heuristic: group by common prefixes for (const callee of callees) { const name = callee.entity.name; // Look for common patterns const patterns = [ { regex: /^validate/, group: 'validation' }, { regex: /^check/, group: 'validation' }, { regex: /^fetch|^get|^load/, group: 'data-fetching' }, { regex: /^save|^store|^persist/, group: 'data-persistence' }, { regex: /^send|^notify|^emit/, group: 'notification' }, { regex: /^format|^render/, group: 'formatting' }, { regex: /^calculate|^compute/, group: 'calculation' }, { regex: /^log|^debug/, group: 'logging' }, ]; let grouped = false; for (const pattern of patterns) { if (pattern.regex.test(name)) { if (!groups.has(pattern.group)) { groups.set(pattern.group, []); } groups.get(pattern.group)!.push(callee); grouped = true; break; } } if (!grouped) { // Default group if (!groups.has('general')) { groups.set('general', []); } groups.get('general')!.push(callee); } } return Array.from(groups.entries()).map(([name, callees]) => ({ name, callees })); } /** * Calculate priority based on smell severity and metrics */ private calculatePriority(smell: CodeSmell, metric: number): RefactoringPriority { if (smell.severity === 'ERROR' || metric > 20) { return 'high'; } else if (smell.severity === 'WARNING' || metric > 10) { return 'medium'; } else { return 'low'; } } /** * Calculate impact score (number of callers) */ private async calculateImpact(projectPath: string, symbolName: string): Promise<number> { try { const hierarchy = await this.deps.queryService.getCallHierarchy( projectPath, symbolName, 1, // depth 1 to count direct callers ); const callerCount = hierarchy.callers.length; return Math.min(10, callerCount); } catch { return 1; // Unknown impact } } /** * Calculate overall score from components */ private calculateOverallScore(score: RefactoringSuggestion['score']): number { // Weighted formula: prioritize high benefit, low risk, low effort const weighted = score.benefit * 0.4 + // 40% benefit (10 - score.risk) * 0.3 + // 30% inverse risk (10 - score.effort) * 0.2 + // 20% inverse effort (10 - score.impact) * 0.1; // 10% inverse impact return Math.round(weighted * 10) / 10; } /** * Estimate time based on complexity */ private estimateTime(units: number): string { if (units <= 2) return '15 minutes'; if (units <= 5) return '30 minutes'; if (units <= 10) return '1 hour'; return '2+ hours'; } /** * Capitalize first letter */ private capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Generate summary statistics */ private generateSummary( suggestions: RefactoringSuggestion[], ): SuggestRefactoringsResult['summary'] { const byPattern: Record<RefactoringPattern, number> = { 'extract-method': 0, 'introduce-parameter-object': 0, 'remove-dead-code': 0, 'inline-function': 0, 'rename-symbol': 0, 'split-function': 0, }; const byPriority: Record<RefactoringPriority, number> = { high: 0, medium: 0, low: 0, }; for (const suggestion of suggestions) { byPattern[suggestion.pattern]++; byPriority[suggestion.priority]++; } return { totalSuggestions: suggestions.length, byPattern, byPriority, }; } }

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/nervusdb/nervusdb-mcp'

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