smartui-migration-tool
Version:
Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI
606 lines (507 loc) ⢠18.6 kB
JavaScript
const { Command, Flags } = require('@oclif/core');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
class SemanticAnalysis extends Command {
static description = 'Perform semantic analysis of codebase for intelligent migration planning';
static flags = {
path: Flags.string({
char: 'p',
description: 'Path to analyze (default: current directory)',
default: process.cwd()
}),
depth: Flags.integer({
char: 'd',
description: 'Analysis depth (1-5, default: 3)',
default: 3
}),
include: Flags.string({
char: 'i',
description: 'File patterns to include (comma-separated)',
default: '**/*.{js,ts,jsx,tsx,py,java,cs}'
}),
exclude: Flags.string({
char: 'e',
description: 'File patterns to exclude (comma-separated)',
default: 'node_modules/**,dist/**,build/**,*.min.js'
}),
output: Flags.string({
char: 'o',
description: 'Output file for analysis results',
default: 'semantic-analysis.json'
}),
verbose: Flags.boolean({
char: 'v',
description: 'Enable verbose output',
default: false
})
};
async run() {
const { flags } = await this.parse(SemanticAnalysis);
console.log(chalk.blue.bold('\nš Semantic Analysis Engine'));
console.log(chalk.gray('Analyzing codebase for intelligent migration planning...\n'));
try {
// Create semantic analysis engine
const semanticEngine = this.createSemanticEngine();
// Perform analysis
const results = await this.performSemanticAnalysis(flags, semanticEngine);
// Display results
this.displayResults(results, flags.verbose);
// Save results
if (flags.output) {
await fs.writeJson(flags.output, results, { spaces: 2 });
console.log(chalk.green(`\nā
Analysis results saved to: ${flags.output}`));
}
} catch (error) {
console.error(chalk.red(`\nā Error during semantic analysis: ${error.message}`));
this.exit(1);
}
}
createSemanticEngine() {
return {
// Intent Detection
detectIntent: (code, context) => {
const intents = [];
// Visual testing intent
if (this.containsVisualTestingPatterns(code)) {
intents.push({
type: 'visual_testing',
confidence: 0.9,
description: 'Code appears to be performing visual testing operations'
});
}
// Test automation intent
if (this.containsTestPatterns(code)) {
intents.push({
type: 'test_automation',
confidence: 0.8,
description: 'Code appears to be test automation related'
});
}
// UI interaction intent
if (this.containsUIPatterns(code)) {
intents.push({
type: 'ui_interaction',
confidence: 0.7,
description: 'Code appears to be performing UI interactions'
});
}
return intents;
},
// Relationship Analysis
analyzeRelationships: (files) => {
const relationships = {
imports: [],
exports: [],
dependencies: [],
callGraph: []
};
files.forEach(file => {
if (file.content) {
// Extract imports
const imports = this.extractImports(file.content, file.language);
relationships.imports.push(...imports.map(imp => ({
file: file.path,
import: imp,
type: 'import'
})));
// Extract exports
const exports = this.extractExports(file.content, file.language);
relationships.exports.push(...exports.map(exp => ({
file: file.path,
export: exp,
type: 'export'
})));
// Extract function calls
const calls = this.extractFunctionCalls(file.content, file.language);
relationships.callGraph.push(...calls.map(call => ({
file: file.path,
function: call,
type: 'call'
})));
}
});
return relationships;
},
// Architecture Pattern Detection
detectArchitecturePatterns: (files) => {
const patterns = [];
// MVC Pattern
if (this.detectMVCPattern(files)) {
patterns.push({
name: 'MVC',
confidence: 0.8,
description: 'Model-View-Controller architecture detected'
});
}
// Component Pattern
if (this.detectComponentPattern(files)) {
patterns.push({
name: 'Component',
confidence: 0.9,
description: 'Component-based architecture detected'
});
}
// Test Pattern
if (this.detectTestPattern(files)) {
patterns.push({
name: 'Test',
confidence: 0.9,
description: 'Test-driven development pattern detected'
});
}
return patterns;
},
// Quality Analysis
analyzeQuality: (files) => {
const quality = {
complexity: 0,
maintainability: 0,
testability: 0,
readability: 0,
issues: []
};
files.forEach(file => {
if (file.content) {
// Calculate complexity
const complexity = this.calculateComplexity(file.content);
quality.complexity += complexity;
// Check maintainability
const maintainability = this.assessMaintainability(file.content);
quality.maintainability += maintainability;
// Check testability
const testability = this.assessTestability(file.content);
quality.testability += testability;
// Check readability
const readability = this.assessReadability(file.content);
quality.readability += readability;
// Identify issues
const issues = this.identifyIssues(file.content, file.path);
quality.issues.push(...issues);
}
});
// Average the metrics
const fileCount = files.length;
quality.complexity = quality.complexity / fileCount;
quality.maintainability = quality.maintainability / fileCount;
quality.testability = quality.testability / fileCount;
quality.readability = quality.readability / fileCount;
return quality;
}
};
}
async performSemanticAnalysis(flags, semanticEngine) {
const results = {
timestamp: new Date().toISOString(),
path: flags.path,
depth: flags.depth,
files: [],
intents: [],
relationships: {},
architecture: [],
quality: {},
recommendations: []
};
// Find files to analyze
const files = await this.findFiles(flags);
results.files = files;
// Analyze each file
for (const file of files) {
try {
const content = await fs.readFile(file.path, 'utf8');
const language = this.detectLanguage(file.path);
const fileInfo = {
path: file.path,
language: language,
content: content,
size: content.length,
lines: content.split('\n').length
};
// Detect intents
const intents = semanticEngine.detectIntent(content, fileInfo);
results.intents.push(...intents.map(intent => ({
...intent,
file: file.path
})));
} catch (error) {
if (flags.verbose) {
console.warn(chalk.yellow(`ā ļø Could not read file: ${file.path}`));
}
}
}
// Analyze relationships
results.relationships = semanticEngine.analyzeRelationships(results.files);
// Detect architecture patterns
results.architecture = semanticEngine.detectArchitecturePatterns(results.files);
// Analyze quality
results.quality = semanticEngine.analyzeQuality(results.files);
// Generate recommendations
results.recommendations = this.generateRecommendations(results);
return results;
}
async findFiles(flags) {
const includePatterns = flags.include.split(',');
const excludePatterns = flags.exclude.split(',');
const files = [];
for (const pattern of includePatterns) {
const matches = glob.sync(pattern, {
cwd: flags.path,
absolute: true,
ignore: excludePatterns
});
files.push(...matches.map(file => ({ path: file })));
}
return files;
}
detectLanguage(filePath) {
const ext = path.extname(filePath).toLowerCase();
const languageMap = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.java': 'java',
'.cs': 'csharp'
};
return languageMap[ext] || 'unknown';
}
containsVisualTestingPatterns(code) {
const patterns = [
/visual|screenshot|snapshot|percy|applitools|smartui/i,
/cy\.screenshot|page\.screenshot|driver\.screenshot/i,
/eyes\.check|eyes\.open|eyes\.close/i,
/percy\.snapshot|percy\.capture/i
];
return patterns.some(pattern => pattern.test(code));
}
containsTestPatterns(code) {
const patterns = [
/describe|it|test|expect|assert/i,
/beforeEach|afterEach|beforeAll|afterAll/i,
/cypress|playwright|selenium|webdriver/i
];
return patterns.some(pattern => pattern.test(code));
}
containsUIPatterns(code) {
const patterns = [
/click|type|select|hover|scroll/i,
/getElementById|querySelector|findElement/i,
/button|input|form|div|span/i
];
return patterns.some(pattern => pattern.test(code));
}
extractImports(content, language) {
const imports = [];
if (language === 'javascript' || language === 'typescript') {
const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1]);
}
} else if (language === 'python') {
const importRegex = /import\s+([a-zA-Z_][a-zA-Z0-9_]*)/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1]);
}
}
return imports;
}
extractExports(content, language) {
const exports = [];
if (language === 'javascript' || language === 'typescript') {
const exportRegex = /export\s+(?:default\s+)?(?:function|class|const|let|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g;
let match;
while ((match = exportRegex.exec(content)) !== null) {
exports.push(match[1]);
}
}
return exports;
}
extractFunctionCalls(content, language) {
const calls = [];
if (language === 'javascript' || language === 'typescript') {
const callRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
let match;
while ((match = callRegex.exec(content)) !== null) {
calls.push(match[1]);
}
}
return calls;
}
detectMVCPattern(files) {
const hasModel = files.some(f => f.path.includes('model') || f.path.includes('Model'));
const hasView = files.some(f => f.path.includes('view') || f.path.includes('View'));
const hasController = files.some(f => f.path.includes('controller') || f.path.includes('Controller'));
return hasModel && hasView && hasController;
}
detectComponentPattern(files) {
const hasComponents = files.some(f => f.path.includes('component') || f.path.includes('Component'));
const hasJSX = files.some(f => f.path.endsWith('.jsx') || f.path.endsWith('.tsx'));
return hasComponents || hasJSX;
}
detectTestPattern(files) {
const hasTestFiles = files.some(f =>
f.path.includes('test') ||
f.path.includes('spec') ||
f.path.includes('__tests__')
);
return hasTestFiles;
}
calculateComplexity(content) {
// Simple complexity calculation based on control structures
const complexity = (content.match(/if|for|while|switch|catch/g) || []).length;
return complexity;
}
assessMaintainability(content) {
// Simple maintainability assessment
const lines = content.split('\n').length;
const comments = (content.match(/\/\/|\/\*|\*\/|#/g) || []).length;
const commentRatio = comments / lines;
return Math.min(commentRatio * 10, 10); // Scale to 0-10
}
assessTestability(content) {
// Simple testability assessment
const hasTests = content.includes('test') || content.includes('spec');
const hasAssertions = content.includes('expect') || content.includes('assert');
return hasTests && hasAssertions ? 8 : 4;
}
assessReadability(content) {
// Simple readability assessment
const lines = content.split('\n').length;
const avgLineLength = content.length / lines;
// Penalize very long lines
return Math.max(0, 10 - (avgLineLength / 100));
}
identifyIssues(content, filePath) {
const issues = [];
// Check for long lines
const lines = content.split('\n');
lines.forEach((line, index) => {
if (line.length > 120) {
issues.push({
type: 'long_line',
severity: 'warning',
line: index + 1,
message: `Line ${index + 1} is too long (${line.length} characters)`
});
}
});
// Check for TODO comments
const todoRegex = /TODO|FIXME|HACK|XXX/gi;
let match;
while ((match = todoRegex.exec(content)) !== null) {
const lineNumber = content.substring(0, match.index).split('\n').length;
issues.push({
type: 'todo',
severity: 'info',
line: lineNumber,
message: `TODO comment found: ${match[0]}`
});
}
return issues;
}
generateRecommendations(results) {
const recommendations = [];
// Quality recommendations
if (results.quality.complexity > 5) {
recommendations.push({
type: 'quality',
priority: 'high',
title: 'Reduce Code Complexity',
description: 'Consider refactoring complex functions to improve maintainability',
action: 'Break down complex functions into smaller, more manageable pieces'
});
}
if (results.quality.maintainability < 5) {
recommendations.push({
type: 'quality',
priority: 'medium',
title: 'Improve Code Documentation',
description: 'Add more comments and documentation to improve maintainability',
action: 'Add inline comments and update documentation'
});
}
// Architecture recommendations
if (results.architecture.length === 0) {
recommendations.push({
type: 'architecture',
priority: 'medium',
title: 'Consider Architecture Patterns',
description: 'No clear architecture patterns detected',
action: 'Consider implementing MVC, Component, or other architectural patterns'
});
}
// Migration recommendations
const visualTestingIntents = results.intents.filter(i => i.type === 'visual_testing');
if (visualTestingIntents.length > 0) {
recommendations.push({
type: 'migration',
priority: 'high',
title: 'Visual Testing Migration',
description: 'Visual testing code detected - consider migrating to SmartUI',
action: 'Use smartui-migrator init to start migration process'
});
}
return recommendations;
}
displayResults(results, verbose) {
console.log(chalk.green.bold('\nš Semantic Analysis Results'));
console.log(chalk.gray('=' * 50));
// File Statistics
console.log(chalk.blue.bold('\nš File Statistics:'));
console.log(` Total files analyzed: ${results.files.length}`);
console.log(` Analysis depth: ${results.depth}`);
console.log(` Analysis path: ${results.path}`);
// Intent Analysis
console.log(chalk.blue.bold('\nšÆ Intent Analysis:'));
const intentGroups = results.intents.reduce((acc, intent) => {
acc[intent.type] = (acc[intent.type] || 0) + 1;
return acc;
}, {});
Object.entries(intentGroups).forEach(([type, count]) => {
console.log(` ${type}: ${count} occurrences`);
});
// Architecture Patterns
console.log(chalk.blue.bold('\nšļø Architecture Patterns:'));
if (results.architecture.length > 0) {
results.architecture.forEach(pattern => {
console.log(` ${pattern.name}: ${pattern.description} (confidence: ${pattern.confidence})`);
});
} else {
console.log(' No clear architecture patterns detected');
}
// Quality Metrics
console.log(chalk.blue.bold('\nš Quality Metrics:'));
console.log(` Complexity: ${results.quality.complexity.toFixed(2)}`);
console.log(` Maintainability: ${results.quality.maintainability.toFixed(2)}/10`);
console.log(` Testability: ${results.quality.testability.toFixed(2)}/10`);
console.log(` Readability: ${results.quality.readability.toFixed(2)}/10`);
// Issues
if (results.quality.issues.length > 0) {
console.log(chalk.blue.bold('\nā ļø Issues Found:'));
const issueGroups = results.quality.issues.reduce((acc, issue) => {
acc[issue.type] = (acc[issue.type] || 0) + 1;
return acc;
}, {});
Object.entries(issueGroups).forEach(([type, count]) => {
console.log(` ${type}: ${count} occurrences`);
});
}
// Recommendations
console.log(chalk.blue.bold('\nš” Recommendations:'));
results.recommendations.forEach((rec, index) => {
const priorityColor = rec.priority === 'high' ? chalk.red : rec.priority === 'medium' ? chalk.yellow : chalk.green;
console.log(` ${index + 1}. ${priorityColor(rec.title)} (${rec.priority})`);
console.log(` ${rec.description}`);
console.log(` Action: ${rec.action}`);
});
if (verbose) {
console.log(chalk.blue.bold('\nš Detailed Analysis:'));
console.log(JSON.stringify(results, null, 2));
}
}
}
module.exports.default = SemanticAnalysis;