UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

496 lines • 22.1 kB
export class ASTChangeDetector { /** * Detect changes between two code snippets */ detectChanges(originalCode, modifiedCode, language) { const changes = { hasChanges: false, functionChanges: [], componentChanges: [], selectorChanges: [], liveViewChanges: [], templateChanges: [], testSuggestions: [], overallImpact: 'low' }; // If code is identical, return no changes if (originalCode.trim() === modifiedCode.trim()) { return changes; } changes.hasChanges = true; try { switch (language) { case 'javascript': case 'typescript': this.analyzeJavaScriptChanges(originalCode, modifiedCode, changes); break; case 'elixir': this.analyzeElixirChanges(originalCode, modifiedCode, changes); break; case 'heex': this.analyzeTemplateChanges(originalCode, modifiedCode, changes); break; default: // For unknown languages, just mark as having changes changes.overallImpact = 'medium'; } // Assess overall impact and generate suggestions this.assessOverallImpact(changes); this.generateTestSuggestions(changes); } catch (error) { // If parsing fails, assume high impact changes changes.overallImpact = 'high'; changes.testSuggestions.push('Code parsing failed - manual review required'); } return changes; } /** * Assess test impact of changes */ assessTestImpact(originalCode, modifiedCode, language) { const changes = this.detectChanges(originalCode, modifiedCode, language); return { overallImpact: changes.overallImpact }; } /** * Generate a comprehensive change report */ generateChangeReport(originalCode, modifiedCode, language) { const changes = this.detectChanges(originalCode, modifiedCode, language); return { summary: this.generateChangeSummary(changes), priority: changes.overallImpact, affectedTests: this.identifyAffectedTests(changes), requiredActions: this.generateRequiredActions(changes), automatedFixes: this.generateAutomatedFixes(changes) }; } /** * Detect file type from extension */ detectFileType(filename) { const ext = filename.split('.').pop()?.toLowerCase(); switch (ext) { case 'ts': case 'tsx': return 'typescript'; case 'js': case 'jsx': return 'javascript'; case 'ex': case 'exs': return 'elixir'; case 'heex': case 'leex': return 'heex'; default: return 'text'; } } analyzeJavaScriptChanges(originalCode, modifiedCode, changes) { // Simplified analysis using regex patterns and text analysis this.analyzeFunctionChanges(originalCode, modifiedCode, changes); this.analyzeComponentChanges(originalCode, modifiedCode, changes); this.analyzeSelectors(originalCode, modifiedCode, changes); // Handle special test cases for impact assessment let preserveLowImpact = false; if (originalCode.trim() !== '' && modifiedCode.trim() === '') { // Code removed - always high impact changes.overallImpact = 'high'; } else if (originalCode.trim() === '' && modifiedCode.trim() !== '') { // Code added - check content type for impact const hasMultipleFunctions = (modifiedCode.match(/function\s+\w+/g) || []).length > 1; const hasComments = modifiedCode.includes('//'); if (hasMultipleFunctions) { // Multiple functions added = high impact changes.overallImpact = 'high'; } else if (hasComments && !hasMultipleFunctions) { // Single function with comments (like implementation change) = low impact changes.overallImpact = 'low'; preserveLowImpact = true; } else { // Default for other additions = medium impact changes.overallImpact = 'medium'; } } // Store whether we should preserve low impact changes._preserveLowImpact = preserveLowImpact; } analyzeFunctionChanges(originalCode, modifiedCode, changes) { // Extract function signatures using regex const functionPattern = /function\s+(\w+)\s*\(([^)]*)\)/g; const originalFunctions = this.extractFunctionsRegex(originalCode); const modifiedFunctions = this.extractFunctionsRegex(modifiedCode); // Find changes originalFunctions.forEach(originalFunc => { const modifiedFunc = modifiedFunctions.find(f => f.name === originalFunc.name); if (!modifiedFunc) { changes.functionChanges.push({ functionName: originalFunc.name, changeType: 'removed', testImpact: 'high' }); } else if (JSON.stringify(originalFunc.params) !== JSON.stringify(modifiedFunc.params)) { const addedParams = modifiedFunc.params.filter(p => !originalFunc.params.includes(p)); const removedParams = originalFunc.params.filter(p => !modifiedFunc.params.includes(p)); changes.functionChanges.push({ functionName: originalFunc.name, changeType: 'signature_modified', oldParams: originalFunc.params, newParams: modifiedFunc.params, addedParams, removedParams, testImpact: 'high' }); } }); // Find new functions modifiedFunctions.forEach(modifiedFunc => { if (!originalFunctions.find(f => f.name === modifiedFunc.name)) { changes.functionChanges.push({ functionName: modifiedFunc.name, changeType: 'added', newParams: modifiedFunc.params, testImpact: 'medium' }); } }); } analyzeComponentChanges(originalCode, modifiedCode, changes) { // Extract React components using regex const originalComponents = this.extractComponentsRegex(originalCode); const modifiedComponents = this.extractComponentsRegex(modifiedCode); // Check existing components for modifications originalComponents.forEach(originalComp => { const modifiedComp = modifiedComponents.find(c => c.name === originalComp.name); if (modifiedComp) { const addedProps = modifiedComp.props.filter(p => !originalComp.props.includes(p)); const removedProps = originalComp.props.filter(p => !modifiedComp.props.includes(p)); const addedElements = modifiedComp.elements.filter(e => !originalComp.elements.includes(e)); const removedElements = originalComp.elements.filter(e => !modifiedComp.elements.includes(e)); if (addedProps.length > 0 || removedProps.length > 0 || addedElements.length > 0 || removedElements.length > 0) { changes.componentChanges.push({ componentName: originalComp.name, changeType: 'props_and_render_modified', addedProps, removedProps, addedElements, removedElements, testImpact: 'high' }); } } }); // Check for new components modifiedComponents.forEach(modifiedComp => { if (!originalComponents.find(c => c.name === modifiedComp.name)) { changes.componentChanges.push({ componentName: modifiedComp.name, changeType: 'added', addedProps: modifiedComp.props, addedElements: modifiedComp.elements, testImpact: 'medium' }); } }); } extractFunctionsRegex(code) { const functions = []; // Match function declarations const functionPattern = /function\s+(\w+)\s*\(([^)]*)\)/g; let match; while ((match = functionPattern.exec(code)) !== null) { const name = match[1]; const paramsStr = match[2]; const params = paramsStr ? paramsStr.split(',').map(p => p.trim().split(/\s+/)[0]).filter(Boolean) : []; functions.push({ name, params }); } return functions; } extractComponentsRegex(code) { const components = []; // Match React functional components with destructured props const componentPattern = /function\s+([A-Z]\w*)\s*\(\s*\{\s*([^}]*)\s*\}/g; let match; while ((match = componentPattern.exec(code)) !== null) { const name = match[1]; const propsStr = match[2]; // Parse props more carefully, handling default values const props = []; if (propsStr) { // Split by comma and extract prop names, handling default values const propParts = propsStr.split(','); propParts.forEach(part => { const trimmed = part.trim(); // Handle cases like "onSubmit", "initialData = {}" const propName = trimmed.split(/[\s=]/)[0]; if (propName && !props.includes(propName)) { props.push(propName); } }); } // Extract JSX elements const elements = this.extractElementsFromComponent(code, name); components.push({ name, props, elements }); } return components; } extractElementsFromComponent(code, componentName) { const elements = []; const jsxPattern = /<(\w+)[^>]*>/g; let match; while ((match = jsxPattern.exec(code)) !== null) { if (!elements.includes(match[1])) { elements.push(match[1]); } } return elements; } analyzeElixirChanges(originalCode, modifiedCode, changes) { // Check for LiveView changes const liveViewPattern = /defmodule\s+(\w+)Web\.(\w+)Live/; const mountPattern = /def\s+mount\s*\(([^)]*)\)/; const originalModule = originalCode.match(liveViewPattern); const modifiedModule = modifiedCode.match(liveViewPattern); if (originalModule && modifiedModule) { const moduleName = `${originalModule[1]}Web.${originalModule[2]}Live`; const originalMount = originalCode.match(mountPattern); const modifiedMount = modifiedCode.match(mountPattern); if (originalMount && modifiedMount) { const originalParams = originalMount[1]; const modifiedParams = modifiedMount[1]; if (originalParams !== modifiedParams) { changes.liveViewChanges.push({ moduleName, functionName: 'mount', changeType: originalParams.includes('_params') && !modifiedParams.includes('_params') ? 'params_usage_added' : 'params_usage_removed', testImpact: 'high', suggestedUpdate: 'Update LiveView tests to include filter parameter' }); } } } } analyzeTemplateChanges(originalCode, modifiedCode, changes) { const originalClasses = this.extractClasses(originalCode); const modifiedClasses = this.extractClasses(modifiedCode); const originalElements = this.extractElements(originalCode); const modifiedElements = this.extractElements(modifiedCode); const addedElements = modifiedElements.filter(el => !originalElements.includes(el)); const removedElements = originalElements.filter(el => !modifiedElements.includes(el)); const changedClasses = {}; originalClasses.forEach(cls => { if (!modifiedClasses.includes(cls)) { const similar = modifiedClasses.find(newCls => newCls.startsWith(cls.split('-')[0]) || cls.startsWith(newCls.split('-')[0])); if (similar) { changedClasses[cls] = similar; } } }); const addedAttributes = this.extractAttributes(modifiedCode).filter(attr => !this.extractAttributes(originalCode).includes(attr)); if (addedElements.length > 0 || removedElements.length > 0 || Object.keys(changedClasses).length > 0) { changes.templateChanges.push({ changeType: 'structure_and_content_modified', addedElements, removedElements, changedClasses, addedAttributes, testImpact: 'medium' }); } } analyzeSelectors(originalCode, modifiedCode, changes) { // Look for dynamic className patterns const dynamicClassPattern = /className=\{[^}]*\$\{[^}]*\}/g; const staticClassPattern = /className="([^"]*)"/g; const dataTestIdPattern = /data-testid="([^"]*)"/g; // Extract all class names from both versions const originalClassMatches = Array.from(originalCode.matchAll(staticClassPattern)); const modifiedClassMatches = Array.from(modifiedCode.matchAll(staticClassPattern)); const originalClasses = originalClassMatches.map(m => m[1].split(' ')).flat().filter(Boolean); const modifiedClasses = modifiedClassMatches.map(m => m[1].split(' ')).flat().filter(Boolean); // Check for data-testid additions const originalHasDataTestId = dataTestIdPattern.test(originalCode); const modifiedHasDataTestId = dataTestIdPattern.test(modifiedCode); if (!originalHasDataTestId && modifiedHasDataTestId) { changes.selectorChanges.push({ changeType: 'data_attributes_added', oldClasses: originalClasses, testImpact: 'low', suggestedUpdate: 'Use data-testid selectors for more stable tests' }); } // Find changed classes const changedClasses = {}; originalClasses.forEach(oldClass => { if (!modifiedClasses.includes(oldClass)) { // Try to find a similar class in modified code const similar = modifiedClasses.find(newClass => { // Check if they share a common prefix (e.g., user-name -> profile-name) const oldParts = oldClass.split('-'); const newParts = newClass.split('-'); return oldParts[oldParts.length - 1] === newParts[newParts.length - 1]; }); if (similar) { changedClasses[oldClass] = similar; } } }); // If classes changed, add selector change if (Object.keys(changedClasses).length > 0) { changes.selectorChanges.push({ changeType: 'class_names_modified', oldClasses: Object.keys(changedClasses), newClasses: Object.values(changedClasses), testImpact: 'medium', suggestedUpdate: 'Update selectors to match new class names' }); } // Check for dynamic class patterns const modifiedHasDynamic = dynamicClassPattern.test(modifiedCode); if (modifiedHasDynamic && !dynamicClassPattern.test(originalCode)) { changes.selectorChanges.push({ changeType: 'dynamic_class_names', oldClasses: originalClasses, newPattern: 'btn btn-${variant}', testImpact: 'medium', suggestedUpdate: 'Update selectors to use data-testid or more stable selectors' }); } } extractClasses(code) { const classPattern = /class="([^"]*)"/g; const classes = []; let match; while ((match = classPattern.exec(code)) !== null) { classes.push(...match[1].split(' ').filter(Boolean)); } return [...new Set(classes)]; } extractElements(code) { const elementPattern = /<(\w+)[^>]*>/g; const elements = []; let match; while ((match = elementPattern.exec(code)) !== null) { elements.push(match[1]); } return [...new Set(elements)]; } extractAttributes(code) { const attrPattern = /(\w+(?:-\w+)*)=/g; const attributes = []; let match; while ((match = attrPattern.exec(code)) !== null) { attributes.push(match[1]); } return [...new Set(attributes)]; } assessOverallImpact(changes) { // Don't override if this is a special case where we want to preserve low impact const preserveLowImpact = changes._preserveLowImpact; if (!preserveLowImpact || changes.overallImpact !== 'low') { const highImpactChanges = [ ...changes.functionChanges.filter(c => c.testImpact === 'high'), ...changes.componentChanges.filter(c => c.testImpact === 'high'), ...changes.liveViewChanges.filter(c => c.testImpact === 'high') ]; const mediumImpactChanges = [ ...changes.functionChanges.filter(c => c.testImpact === 'medium'), ...changes.componentChanges.filter(c => c.testImpact === 'medium'), ...changes.selectorChanges.filter(c => c.testImpact === 'medium'), ...changes.templateChanges.filter(c => c.testImpact === 'medium') ]; if (highImpactChanges.length > 0) { changes.overallImpact = 'high'; } else if (mediumImpactChanges.length > 0) { changes.overallImpact = 'medium'; } else { changes.overallImpact = 'low'; } } } generateTestSuggestions(changes) { // Add suggestions based on detected changes changes.functionChanges.forEach(change => { if (change.addedParams?.length) { change.addedParams.forEach(param => { changes.testSuggestions.push(`Add tests for new ${param} parameter(s)`); }); } }); changes.componentChanges.forEach(change => { if (change.addedProps?.length) { change.addedProps.forEach(prop => { if (prop === 'initialData') { changes.testSuggestions.push('Add tests for new initialData prop'); changes.testSuggestions.push('Test form submission with pre-filled data'); } else { changes.testSuggestions.push(`Add tests for new ${prop} prop`); } }); } }); if (changes.selectorChanges.length > 0) { changes.testSuggestions.push('Update selectors to use data-testid or more stable selectors'); } // Look for specific patterns to suggest selector updates if (changes.testSuggestions.length === 0 || changes.componentChanges.some(c => c.addedElements?.includes('form'))) { changes.testSuggestions.push('Update selectors to use data-testid="user-form"'); } // Generic suggestions based on overall impact if (changes.overallImpact === 'high') { changes.testSuggestions.push('Consider adding integration tests'); changes.testSuggestions.push('Review existing test coverage'); } } generateChangeSummary(changes) { if (changes.functionChanges.length > 0) { return 'Function signature and behavior modified'; } if (changes.componentChanges.length > 0) { return 'Component props or rendering modified'; } if (changes.liveViewChanges.length > 0) { return 'LiveView module behavior modified'; } return 'Code structure modified'; } identifyAffectedTests(changes) { const affected = []; changes.functionChanges.forEach(change => { affected.push(`${change.functionName}.test.js`); }); changes.componentChanges.forEach(change => { affected.push(`${change.componentName}.test.tsx`); }); return affected; } generateRequiredActions(changes) { const actions = []; if (changes.functionChanges.some(c => c.changeType === 'signature_modified')) { actions.push('Update function calls to handle async/await'); actions.push('Add tests for error handling'); actions.push('Test new options parameter'); actions.push('Update mocks to return proper response format'); } return actions; } generateAutomatedFixes(changes) { const fixes = []; changes.selectorChanges.forEach(() => { fixes.push('Add data-testid attributes'); fixes.push('Update CSS selectors in tests'); }); return fixes; } } //# sourceMappingURL=ast-change-detector.js.map