UNPKG

bugger-mcp

Version:

MCP Server for managing bugs, feature requests, and improvements

539 lines 19 kB
// Modern code analysis using Tree-sitter for accurate AST parsing import Parser from 'tree-sitter'; import JavaScript from 'tree-sitter-javascript'; import TypeScript from 'tree-sitter-typescript'; import Java from 'tree-sitter-java'; import * as fs from 'fs'; import * as path from 'path'; /** * Modern code analyzer using Tree-sitter for accurate AST parsing */ export class TreeSitterCodeAnalyzer { rootPath; parsers = new Map(); defaultExtensions = ['.js', '.ts', '.jsx', '.tsx', '.java']; defaultExcludePatterns = [ 'node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__', '.venv', 'venv', 'target', 'bin', 'obj' ]; constructor(rootPath = process.cwd()) { this.rootPath = rootPath; this.initializeParsers(); } initializeParsers() { // JavaScript/JSX parser const jsParser = new Parser(); jsParser.setLanguage(JavaScript); this.parsers.set('.js', jsParser); this.parsers.set('.jsx', jsParser); // TypeScript parser const tsParser = new Parser(); tsParser.setLanguage(TypeScript.typescript); this.parsers.set('.ts', tsParser); const tsxParser = new Parser(); tsxParser.setLanguage(TypeScript.tsx); this.parsers.set('.tsx', tsxParser); // Java parser const javaParser = new Parser(); javaParser.setLanguage(Java); this.parsers.set('.java', javaParser); } /** * 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 matches = await this.extractFunctionDefinitions(file, functionName); functionMatches.push(...matches); } catch (error) { console.error(`Error analyzing 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 matches = await this.extractClassDefinitions(file, className); classMatches.push(...matches); } catch (error) { console.error(`Error analyzing file ${file}:`, error); } } return classMatches.sort((a, b) => b.relevanceScore - a.relevanceScore); } /** * Find similar code patterns using structural similarity */ async findSimilarPatterns(targetPattern, options = this.getDefaultPatternSearchOptions()) { const files = await this.traverseCodebase(this.getDefaultTraversalOptions()); const patternMatches = []; // Parse the target pattern to get its AST structure const targetStructure = this.parsePatternStructure(targetPattern); if (!targetStructure) return []; for (const file of files) { try { const matches = await this.findStructuralMatches(file, targetStructure, options); patternMatches.push(...matches); } catch (error) { console.error(`Error analyzing patterns in ${file}:`, error); } } return patternMatches .filter(match => match.similarity >= options.minSimilarity) .sort((a, b) => b.similarity - a.similarity) .slice(0, options.maxResults); } /** * Extract function definitions from a file using AST parsing */ async extractFunctionDefinitions(filePath, targetFunction) { const content = fs.readFileSync(filePath, 'utf8'); const ext = path.extname(filePath); const parser = this.parsers.get(ext); if (!parser) return []; const tree = parser.parse(content); const functions = []; this.traverseAST(tree.rootNode, (node) => { if (this.isFunctionNode(node)) { const functionData = this.extractFunctionData(node, content, filePath); if (functionData && (!targetFunction || functionData.name === targetFunction)) { functions.push(functionData); } } }); return functions; } /** * Extract class definitions from a file using AST parsing */ async extractClassDefinitions(filePath, targetClass) { const content = fs.readFileSync(filePath, 'utf8'); const ext = path.extname(filePath); const parser = this.parsers.get(ext); if (!parser) return []; const tree = parser.parse(content); const classes = []; this.traverseAST(tree.rootNode, (node) => { if (this.isClassNode(node)) { const classData = this.extractClassData(node, content, filePath); if (classData && (!targetClass || classData.name === targetClass)) { classes.push(classData); } } }); return classes; } /** * Traverse AST nodes with a callback */ traverseAST(node, callback) { callback(node); for (let i = 0; i < node.childCount; i++) { this.traverseAST(node.child(i), callback); } } /** * Check if a node represents a function */ isFunctionNode(node) { return [ 'function_declaration', 'function_expression', 'arrow_function', 'method_definition', 'function_definition' ].includes(node.type); } /** * Check if a node represents a class */ isClassNode(node) { return [ 'class_declaration', 'interface_declaration', 'class_definition' ].includes(node.type); } /** * Extract function data from AST node */ extractFunctionData(node, content, filePath) { const nameNode = this.findChildByType(node, ['identifier', 'property_identifier']); if (!nameNode) return null; const name = nameNode.text; const startLine = node.startPosition.row + 1; const endLine = node.endPosition.row + 1; const signature = this.getNodeText(node, content); // Extract parameters const parameters = []; const paramsNode = this.findChildByType(node, ['formal_parameters', 'parameters']); if (paramsNode) { this.traverseAST(paramsNode, (child) => { if (child.type === 'identifier' && child.parent?.type !== 'type_annotation') { parameters.push(child.text); } }); } // Check if async const isAsync = signature.includes('async'); // Check if it's a method const isMethod = node.parent?.type === 'class_body' || node.parent?.type === 'method_definition'; // Get class name if it's a method let className; if (isMethod) { let parent = node.parent; while (parent && parent.type !== 'class_declaration') { parent = parent.parent; } if (parent) { const classNameNode = this.findChildByType(parent, ['identifier']); if (classNameNode) { className = classNameNode.text; } } } return { name, filePath, startLine, endLine, signature: signature.split('\n')[0], // First line of signature parameters, isAsync, isMethod, ...(className && { className }), relevanceScore: this.calculateFunctionRelevance(name, signature) }; } /** * Extract class data from AST node */ extractClassData(node, content, filePath) { const nameNode = this.findChildByType(node, ['identifier', 'type_identifier']); if (!nameNode) return null; const name = nameNode.text; const startLine = node.startPosition.row + 1; const endLine = node.endPosition.row + 1; // Extract extends/implements let extendsClass; const implementsList = []; const extendsClause = this.findChildByType(node, ['extends_clause', 'class_heritage']); if (extendsClause) { const extendsIdentifier = this.findChildByType(extendsClause, ['identifier', 'type_identifier']); if (extendsIdentifier) { extendsClass = extendsIdentifier.text; } } const implementsClause = this.findChildByType(node, ['implements_clause']); if (implementsClause) { this.traverseAST(implementsClause, (child) => { if (child.type === 'identifier' || child.type === 'type_identifier') { implementsList.push(child.text); } }); } // Extract methods and properties const methods = []; const properties = []; const classBody = this.findChildByType(node, ['class_body']); if (classBody) { this.traverseAST(classBody, (child) => { if (this.isFunctionNode(child)) { const methodName = this.findChildByType(child, ['identifier', 'property_identifier']); if (methodName) { methods.push(methodName.text); } } else if (child.type === 'field_definition' || child.type === 'property_definition') { const propName = this.findChildByType(child, ['identifier', 'property_identifier']); if (propName) { properties.push(propName.text); } } }); } const isInterface = node.type === 'interface_declaration'; const isAbstract = this.getNodeText(node, content).includes('abstract'); return { name, filePath, startLine, endLine, ...(extendsClass && { extendsClass }), implements: implementsList, methods, properties, isAbstract, isInterface, relevanceScore: this.calculateClassRelevance(name, isInterface, isAbstract) }; } /** * Parse pattern structure for similarity matching */ parsePatternStructure(pattern) { // Try to parse the pattern with each available parser for (const [ext, parser] of this.parsers) { try { const tree = parser.parse(pattern); if (tree.rootNode.hasError) continue; return { tree, structure: this.extractStructuralFeatures(tree.rootNode) }; } catch (error) { continue; } } return null; } /** * Extract structural features from AST for similarity comparison */ extractStructuralFeatures(node) { const features = { type: node.type, childCount: node.childCount, childTypes: [], depth: 0 }; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); features.childTypes.push(child.type); } return features; } /** * Find structural matches in a file */ async findStructuralMatches(filePath, targetStructure, options) { const content = fs.readFileSync(filePath, 'utf8'); const ext = path.extname(filePath); const parser = this.parsers.get(ext); if (!parser) return []; const tree = parser.parse(content); const matches = []; this.traverseAST(tree.rootNode, (node) => { const similarity = this.calculateStructuralSimilarity(node, targetStructure.tree.rootNode); if (similarity >= options.minSimilarity) { matches.push({ pattern: targetStructure.tree.rootNode.text, filePath, startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1, similarity, context: this.extractContext(node, content), matchType: this.determineMatchType(similarity) }); } }); return matches; } /** * Calculate structural similarity between two AST nodes */ calculateStructuralSimilarity(node1, node2) { if (node1.type !== node2.type) return 0; let similarity = 0.5; // Base similarity for same type // Compare child structure const childTypeSimilarity = this.compareChildTypes(node1, node2); similarity += childTypeSimilarity * 0.3; // Compare depth and complexity const complexitySimilarity = this.compareComplexity(node1, node2); similarity += complexitySimilarity * 0.2; return Math.min(1.0, similarity); } /** * Compare child types between two nodes */ compareChildTypes(node1, node2) { const types1 = []; const types2 = []; for (let i = 0; i < node1.childCount; i++) { types1.push(node1.child(i).type); } for (let i = 0; i < node2.childCount; i++) { types2.push(node2.child(i).type); } if (types1.length === 0 && types2.length === 0) return 1.0; if (types1.length === 0 || types2.length === 0) return 0.0; const intersection = types1.filter(type => types2.includes(type)); const union = [...new Set([...types1, ...types2])]; return intersection.length / union.length; } /** * Compare complexity between two nodes */ compareComplexity(node1, node2) { const depth1 = this.getNodeDepth(node1); const depth2 = this.getNodeDepth(node2); const maxDepth = Math.max(depth1, depth2); if (maxDepth === 0) return 1.0; return 1.0 - Math.abs(depth1 - depth2) / maxDepth; } /** * Get the depth of an AST node */ getNodeDepth(node) { let maxDepth = 0; for (let i = 0; i < node.childCount; i++) { const childDepth = this.getNodeDepth(node.child(i)); maxDepth = Math.max(maxDepth, childDepth); } return maxDepth + 1; } /** * Extract context around a node */ extractContext(node, content) { const lines = content.split('\n'); const startLine = Math.max(0, node.startPosition.row - 2); const endLine = Math.min(lines.length, node.endPosition.row + 3); return lines.slice(startLine, endLine); } /** * Determine match type based on similarity score */ determineMatchType(similarity) { if (similarity > 0.9) return 'exact'; if (similarity > 0.6) return 'structural'; return 'semantic'; } /** * Find child node by type */ findChildByType(node, types) { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (types.includes(child.type)) { return child; } } return null; } /** * Get text content of a node */ getNodeText(node, content) { return content.slice(node.startIndex, node.endIndex); } /** * Calculate function relevance score */ calculateFunctionRelevance(name, signature) { let score = 0.5; if (name.length > 3) score += 0.1; if (/^[a-z][a-z0-9]*([A-Z][a-z0-9]*)*$/.test(name)) score += 0.1; if (signature.includes('(') && signature.includes(')')) score += 0.2; if (signature.includes('async')) score += 0.1; return Math.min(1.0, score); } /** * Calculate class relevance score */ calculateClassRelevance(name, isInterface, isAbstract) { let score = 0.5; if (/^[A-Z][a-z0-9]*([A-Z][a-z0-9]*)*$/.test(name)) score += 0.2; if (isInterface) score += 0.1; if (isAbstract) score += 0.1; return Math.min(1.0, score); } /** * 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); 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); } } } } catch (error) { console.error(`Error traversing directory ${dir}:`, error); } }; traverse(this.rootPath); return files; } shouldExclude(filePath, excludePatterns) { return excludePatterns.some(pattern => filePath.includes(pattern) || filePath.includes(path.sep + pattern + path.sep)); } 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 }; } } //# sourceMappingURL=treesitter-code-analyzer.js.map