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
JavaScript
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