UNPKG

bugger-mcp

Version:

MCP Server for managing bugs, feature requests, and improvements

562 lines 22.6 kB
// Code pattern matching system for finding functions, classes, and similar patterns import * as fs from 'fs'; import * as path from 'path'; /** * Main code pattern matching engine */ export class CodePatternMatcher { rootPath; defaultExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.rb', '.php', '.go', '.cs']; defaultExcludePatterns = [ 'node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__', '.venv', 'venv', 'target', 'bin', 'obj' ]; constructor(rootPath = process.cwd()) { this.rootPath = rootPath; } /** * Find function definitions across the codebase */ async findFunctionDefinitions(functionName, options = this.getDefaultTraversalOptions()) { const files = await this.traverseCodebase(options); const functionMatches = []; for (const file of files) { try { const content = fs.readFileSync(file, 'utf8'); const matches = this.extractFunctionDefinitions(content, file, functionName); functionMatches.push(...matches); } catch (error) { console.error(`Error reading file ${file}:`, error); } } return functionMatches.sort((a, b) => b.relevanceScore - a.relevanceScore); } /** * Find class definitions across the codebase */ async findClassDefinitions(className, options = this.getDefaultTraversalOptions()) { const files = await this.traverseCodebase(options); const classMatches = []; for (const file of files) { try { const content = fs.readFileSync(file, 'utf8'); const matches = this.extractClassDefinitions(content, file, className); classMatches.push(...matches); } catch (error) { console.error(`Error reading file ${file}:`, error); } } return classMatches.sort((a, b) => b.relevanceScore - a.relevanceScore); } /** * Find similar code patterns to a given pattern */ async findSimilarPatterns(targetPattern, options = this.getDefaultPatternSearchOptions()) { const files = await this.traverseCodebase(this.getDefaultTraversalOptions()); const patternMatches = []; const normalizedTarget = this.normalizePattern(targetPattern, options); for (const file of files) { try { const content = fs.readFileSync(file, 'utf8'); const matches = this.findPatternMatches(content, file, normalizedTarget, options); patternMatches.push(...matches); } catch (error) { console.error(`Error reading file ${file}:`, error); } } return patternMatches .filter(match => match.similarity >= options.minSimilarity) .sort((a, b) => b.similarity - a.similarity) .slice(0, options.maxResults); } /** * Traverse the codebase and collect relevant files */ async traverseCodebase(options) { const files = []; const traverse = (dir, currentDepth = 0) => { if (currentDepth > options.maxDepth) return; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Skip excluded patterns if (this.shouldExclude(fullPath, options.excludePatterns)) { continue; } if (entry.isDirectory()) { traverse(fullPath, currentDepth + 1); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (options.includeExtensions.includes(ext)) { files.push(fullPath); } } else if (entry.isSymbolicLink() && options.followSymlinks) { const resolvedPath = fs.readlinkSync(fullPath); if (fs.existsSync(resolvedPath)) { const stat = fs.statSync(resolvedPath); if (stat.isDirectory()) { traverse(resolvedPath, currentDepth + 1); } else if (stat.isFile()) { const ext = path.extname(resolvedPath).toLowerCase(); if (options.includeExtensions.includes(ext)) { files.push(resolvedPath); } } } } } } catch (error) { console.error(`Error traversing directory ${dir}:`, error); } }; traverse(this.rootPath); return files; } /** * Extract function definitions from file content */ extractFunctionDefinitions(content, filePath, targetFunction) { const functions = []; const lines = content.split('\n'); // Multiple language patterns for function definitions const patterns = this.getFunctionPatterns(path.extname(filePath)); for (const pattern of patterns) { let match; const regex = new RegExp(pattern.regex, 'gm'); while ((match = regex.exec(content)) !== null) { const functionName = match[pattern.nameGroup]; // Skip if we're looking for a specific function and this isn't it if (targetFunction && functionName !== targetFunction) { continue; } const startLine = this.getLineNumber(content, match.index); const endLine = this.findFunctionEndLine(content, match.index, startLine); const returnType = match[pattern.returnGroup || 0] || undefined; const className = pattern.classGroup ? match[pattern.classGroup] || undefined : undefined; const accessibility = pattern.accessGroup ? match[pattern.accessGroup] || undefined : undefined; const functionMatch = { name: functionName, filePath, startLine, endLine, signature: match[0].trim(), parameters: this.extractParameters(match[pattern.paramsGroup || 0] || ''), ...(returnType && { returnType }), isAsync: pattern.isAsync ? /\basync\b/.test(match[0]) : false, isMethod: pattern.isMethod || false, ...(className && { className }), ...(accessibility && { accessibility }), relevanceScore: this.calculateFunctionRelevance(functionName, match[0], content) }; functions.push(functionMatch); } } return functions; } /** * Extract class definitions from file content */ extractClassDefinitions(content, filePath, targetClass) { const classes = []; const lines = content.split('\n'); // Multiple language patterns for class definitions const patterns = this.getClassPatterns(path.extname(filePath)); for (const pattern of patterns) { let match; const regex = new RegExp(pattern.regex, 'gm'); while ((match = regex.exec(content)) !== null) { const className = match[pattern.nameGroup]; // Skip if we're looking for a specific class and this isn't it if (targetClass && className !== targetClass) { continue; } const startLine = this.getLineNumber(content, match.index); const endLine = this.findClassEndLine(content, match.index, startLine); const extendsClass = match[pattern.extendsGroup || 0] || undefined; const classMatch = { name: className, filePath, startLine, endLine, ...(extendsClass && { extendsClass }), implements: this.extractImplements(match[pattern.implementsGroup || 0] || ''), methods: this.extractClassMethods(content, startLine, endLine), properties: this.extractClassProperties(content, startLine, endLine), isAbstract: pattern.isAbstract ? /\babstract\b/.test(match[0]) : false, isInterface: pattern.isInterface || false, relevanceScore: this.calculateClassRelevance(className, match[0], content) }; classes.push(classMatch); } } return classes; } /** * Find pattern matches in file content */ findPatternMatches(content, filePath, normalizedTarget, options) { const matches = []; const lines = content.split('\n'); // Split content into logical blocks (functions, classes, etc.) const blocks = this.extractCodeBlocks(content); for (const block of blocks) { const normalizedBlock = this.normalizePattern(block.content, options); // Calculate similarity using multiple metrics const similarity = this.calculatePatternSimilarity(normalizedTarget, normalizedBlock); if (similarity >= options.minSimilarity) { matches.push({ pattern: normalizedTarget, filePath, startLine: block.startLine, endLine: block.endLine, similarity, context: this.extractBlockContext(content, block.startLine, block.endLine), matchType: this.determineMatchType(similarity) }); } } return matches; } // Helper methods getDefaultTraversalOptions() { return { includeExtensions: this.defaultExtensions, excludePatterns: this.defaultExcludePatterns, maxDepth: 10, followSymlinks: false, ignoreCase: true }; } getDefaultPatternSearchOptions() { return { maxResults: 50, minSimilarity: 0.3, includeComments: false, normalizeWhitespace: true, caseSensitive: false }; } shouldExclude(filePath, excludePatterns) { return excludePatterns.some(pattern => filePath.includes(pattern) || filePath.includes(path.sep + pattern + path.sep)); } getFunctionPatterns(extension) { const patterns = []; if (['.js', '.ts', '.jsx', '.tsx'].includes(extension)) { patterns.push( // Regular function declarations { regex: '(?:^|\\s)(async\\s+)?function\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*\\(([^)]*)\\)(?:\\s*:\\s*([^{]+))?', nameGroup: 2, paramsGroup: 3, returnGroup: 4, isAsync: true }, // Arrow functions { regex: '(?:^|\\s)(?:const|let|var)\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=\\s*(?:async\\s+)?\\(([^)]*)\\)\\s*=>', nameGroup: 1, paramsGroup: 2, isAsync: true }, // Method definitions { regex: '(?:^|\\s)(public|private|protected)?\\s*(async\\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*\\(([^)]*)\\)(?:\\s*:\\s*([^{]+))?\\s*{', nameGroup: 3, paramsGroup: 4, returnGroup: 5, accessGroup: 1, isAsync: true, isMethod: true }); } else if (extension === '.py') { patterns.push( // Python function definitions { regex: '(?:^|\\s)def\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)(?:\\s*->\\s*([^:]+))?:', nameGroup: 1, paramsGroup: 2, returnGroup: 3 }, // Python class methods { regex: '(?:^|\\s)def\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(\\s*self\\s*(?:,\\s*([^)]*))??\\)(?:\\s*->\\s*([^:]+))?:', nameGroup: 1, paramsGroup: 2, returnGroup: 3, isMethod: true }); } else if (extension === '.java') { patterns.push( // Java method definitions { regex: '(?:^|\\s)(public|private|protected)?\\s*(static\\s+)?(async\\s+)?([a-zA-Z_$][a-zA-Z0-9_$<>\\[\\]]*)?\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*\\(([^)]*)\\)', nameGroup: 5, paramsGroup: 6, returnGroup: 4, accessGroup: 1, isMethod: true }); } return patterns; } getClassPatterns(extension) { const patterns = []; if (['.js', '.ts', '.jsx', '.tsx'].includes(extension)) { patterns.push( // Class declarations { regex: '(?:^|\\s)(abstract\\s+)?class\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\\s+extends\\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?(?:\\s+implements\\s+([^{]+))?', nameGroup: 2, extendsGroup: 3, implementsGroup: 4, isAbstract: true }, // Interface declarations { regex: '(?:^|\\s)interface\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\\s+extends\\s+([^{]+))?', nameGroup: 1, extendsGroup: 2, isInterface: true }); } else if (extension === '.py') { patterns.push( // Python class definitions { regex: '(?:^|\\s)class\\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\\s*\\(\\s*([^)]*)\\s*\\))?:', nameGroup: 1, extendsGroup: 2 }); } else if (extension === '.java') { patterns.push( // Java class definitions { regex: '(?:^|\\s)(public\\s+)?(abstract\\s+)?(final\\s+)?class\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\\s+extends\\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?(?:\\s+implements\\s+([^{]+))?', nameGroup: 4, extendsGroup: 5, implementsGroup: 6, isAbstract: true }, // Java interface definitions { regex: '(?:^|\\s)(public\\s+)?interface\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\\s+extends\\s+([^{]+))?', nameGroup: 2, extendsGroup: 3, isInterface: true }); } return patterns; } getLineNumber(content, position) { return content.substring(0, position).split('\n').length; } findFunctionEndLine(content, startPosition, startLine) { const lines = content.split('\n'); let braceCount = 0; let inFunction = false; for (let i = startLine - 1; i < lines.length; i++) { const line = lines[i]; for (const char of line) { if (char === '{') { braceCount++; inFunction = true; } else if (char === '}') { braceCount--; if (inFunction && braceCount === 0) { return i + 1; } } } } return startLine + 10; // Default fallback } findClassEndLine(content, startPosition, startLine) { // Similar logic to findFunctionEndLine but for classes return this.findFunctionEndLine(content, startPosition, startLine); } extractParameters(paramsString) { if (!paramsString || paramsString.trim() === '') { return []; } return paramsString.split(',').map(param => param.trim()).filter(param => param); } extractImplements(implementsString) { if (!implementsString || implementsString.trim() === '') { return []; } return implementsString.split(',').map(impl => impl.trim()).filter(impl => impl); } extractClassMethods(content, startLine, endLine) { const methods = []; const lines = content.split('\n').slice(startLine - 1, endLine); const classContent = lines.join('\n'); const methodRegex = /(?:^|\s)(public|private|protected)?\s*(async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/gm; let match; while ((match = methodRegex.exec(classContent)) !== null) { methods.push(match[3]); } return methods; } extractClassProperties(content, startLine, endLine) { const properties = []; const lines = content.split('\n').slice(startLine - 1, endLine); const classContent = lines.join('\n'); const propertyRegex = /(?:^|\s)(public|private|protected)?\s+(readonly\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[:=]/gm; let match; while ((match = propertyRegex.exec(classContent)) !== null) { properties.push(match[3]); } return properties; } calculateFunctionRelevance(name, signature, content) { let score = 0.5; // Base score // Boost for descriptive names if (name.length > 3) score += 0.1; if (/^[a-z][a-z0-9]*([A-Z][a-z0-9]*)*$/.test(name)) score += 0.1; // camelCase // Boost for functions with parameters if (signature.includes('(') && signature.includes(')')) { const paramsMatch = signature.match(/\(([^)]*)\)/); if (paramsMatch && paramsMatch[1].trim()) { score += 0.2; } } // Boost for async functions if (signature.includes('async')) score += 0.1; // Boost for JSDoc comments if (content.includes('/**') && content.includes('*/')) score += 0.1; return Math.min(1.0, score); } calculateClassRelevance(name, signature, content) { let score = 0.5; // Base score // Boost for PascalCase class names if (/^[A-Z][a-z0-9]*([A-Z][a-z0-9]*)*$/.test(name)) score += 0.2; // Boost for classes with inheritance if (signature.includes('extends')) score += 0.1; if (signature.includes('implements')) score += 0.1; // Boost for abstract classes if (signature.includes('abstract')) score += 0.1; // Boost for JSDoc comments if (content.includes('/**') && content.includes('*/')) score += 0.1; return Math.min(1.0, score); } normalizePattern(pattern, options) { let normalized = pattern; if (options.normalizeWhitespace) { normalized = normalized.replace(/\s+/g, ' ').trim(); } if (!options.caseSensitive) { normalized = normalized.toLowerCase(); } if (!options.includeComments) { // Remove single-line comments normalized = normalized.replace(/\/\/.*$/gm, ''); // Remove multi-line comments normalized = normalized.replace(/\/\*[\s\S]*?\*\//g, ''); } return normalized; } extractCodeBlocks(content) { const blocks = []; const lines = content.split('\n'); let blockStart = 0; let braceCount = 0; let inBlock = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const char of line) { if (char === '{') { if (!inBlock) { blockStart = i; inBlock = true; } braceCount++; } else if (char === '}') { braceCount--; if (inBlock && braceCount === 0) { blocks.push({ content: lines.slice(blockStart, i + 1).join('\n'), startLine: blockStart + 1, endLine: i + 1 }); inBlock = false; } } } } return blocks; } calculatePatternSimilarity(pattern1, pattern2) { // Simple similarity calculation using Levenshtein distance const distance = this.levenshteinDistance(pattern1, pattern2); const maxLength = Math.max(pattern1.length, pattern2.length); if (maxLength === 0) return 1.0; return 1.0 - (distance / maxLength); } levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); } } } return matrix[str2.length][str1.length]; } extractBlockContext(content, startLine, endLine) { const lines = content.split('\n'); const contextBefore = lines.slice(Math.max(0, startLine - 3), startLine - 1); const contextAfter = lines.slice(endLine, Math.min(lines.length, endLine + 3)); return [...contextBefore, ...contextAfter]; } determineMatchType(similarity) { if (similarity > 0.9) return 'exact'; if (similarity > 0.6) return 'structural'; return 'semantic'; } } //# sourceMappingURL=code-pattern-matching.js.map