UNPKG

smartui-migration-tool

Version:

Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI

606 lines (507 loc) • 18.6 kB
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;