UNPKG

agentsqripts

Version:

Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems

439 lines (368 loc) • 12.8 kB
#!/usr/bin/env node /** * @file Utility function promotion script * @description Automatically promotes utility functions to exports and updates index files based on the refactoring analysis */ const fs = require('fs'); const path = require('path'); const { getProcessArgs } = require('../lib/utils/processHelpers'); const { analyzeProjectRefactoring, generateRefactoringSuggestions } = require('../lib/refactoringAnalysis'); const { DEFAULT_CONFIG, PATTERNS } = require('../config/localVars'); const { parseArgs: sharedParseArgs } = require('./lib/argumentParser'); /** * Show help information */ function showHelp() { console.log(`Usage: node promote-exports.js [OPTIONS] <path> Automatically promotes utility functions to exports and updates index files for better code organization. OPTIONS: --dry-run Show what would be changed without making modifications --target-dirs <dirs> Comma-separated list of target utility directories --extensions <exts> Comma-separated list of file extensions (default: .js,.ts,.jsx,.tsx) --index-files <files> Comma-separated list of index file names (default: index.ts,index.js) --skip-private Skip functions starting with underscore (default: true) --help Show this help message EXAMPLES: node promote-exports.js . node promote-exports.js --dry-run --target-dirs lib,src/utils . node promote-exports.js --extensions .js,.ts --skip-private src/ WHAT IT DOES: 1. Analyzes project for unexported utility functions 2. Adds 'export' keyword to eligible functions and classes 3. Updates or creates index files with appropriate re-exports 4. Provides detailed reporting of all changes made SAFETY: - Skips functions starting with underscore (private/internal) - Only modifies files in specified target directories - Uses --dry-run to preview changes before applying them`); } /** * Promote exports in a single file */ async function promoteExportsInFile(filePath, options = {}) { const { skipPrivate = true } = options; try { const content = await fs.promises.readFile(filePath, 'utf8'); const lines = content.split('\n'); let changed = false; const promoted = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip already exported items if (PATTERNS.export.test(line)) { continue; } // Check for function declarations const functionMatch = line.match(PATTERNS.functionDeclaration); if (functionMatch) { const name = functionMatch[1]; // Skip private functions if option is set if (skipPrivate && name.startsWith('_')) { continue; } // Skip if name contains 'internal' (case insensitive) if (name.toLowerCase().includes('internal')) { continue; } lines[i] = `export ${line}`; promoted.push({ type: 'function', name, line: i + 1, declarationType: 'function', content: line.trim() }); changed = true; } // Check for arrow functions const arrowMatch = line.match(PATTERNS.arrowFunction); if (arrowMatch) { const name = arrowMatch[1]; // Skip private functions if option is set if (skipPrivate && name.startsWith('_')) { continue; } // Skip if name contains 'internal' (case insensitive) if (name.toLowerCase().includes('internal')) { continue; } lines[i] = `export ${line}`; promoted.push({ type: 'function', name, line: i + 1, declarationType: 'const', content: line.trim() }); changed = true; } // Check for class declarations const classMatch = line.match(PATTERNS.classDeclaration); if (classMatch) { const className = classMatch[1]; // Skip private classes if (skipPrivate && className.startsWith('_')) { continue; } lines[i] = `export ${line}`; promoted.push({ type: 'class', name: className, line: i + 1, declarationType: 'class', content: line.trim() }); changed = true; } } return { filePath, promoted, changed, newContent: changed ? lines.join('\n') : content }; } catch (error) { return { filePath, error: error.message }; } } /** * Get or create index file for a directory */ async function getIndexFile(directory, options = {}) { const { indexFiles = DEFAULT_CONFIG.indexFiles } = options; // Check if any index file already exists for (const fileName of indexFiles) { const indexPath = path.join(directory, fileName); try { await fs.promises.access(indexPath); return indexPath; } catch (error) { // File doesn't exist, continue to next } } // Create new index file (prefer .ts if no existing file found) const newIndexPath = path.join(directory, indexFiles[0] || 'index.ts'); return newIndexPath; } /** * Update index file with new exports */ async function updateIndexFile(indexPath, promotedItems, sourceFile, options = {}) { const { dryRun = false } = options; const relativePath = './' + path.basename(sourceFile, path.extname(sourceFile)); const updates = []; let existingContent = ''; let isNewFile = true; try { existingContent = await fs.promises.readFile(indexPath, 'utf8'); isNewFile = false; } catch (error) { // File doesn't exist, will be created } for (const { name } of promotedItems) { const exportLine = `export { ${name} } from '${relativePath}';`; if (!existingContent.includes(exportLine)) { updates.push(exportLine); if (!dryRun) { await fs.promises.appendFile(indexPath, exportLine + '\n'); } } } return { indexPath, updates, isNewFile }; } /** * Promote exports across the project */ async function promoteProjectExports(projectPath, options = {}) { const { targetDirs = DEFAULT_CONFIG.targetDirs, dryRun = false, skipPrivate = true } = options; console.log(`šŸ”§ ${dryRun ? 'Analyzing' : 'Promoting'} exports in: ${projectPath}`); console.log(`šŸ“ Target directories: ${targetDirs.join(', ')}`); console.log(`šŸ” Extensions: ${options.extensions?.join(', ') || DEFAULT_CONFIG.validExtensions.join(', ')}`); console.log(`${dryRun ? 'šŸ‘€ DRY RUN MODE - No changes will be made' : 'āœļø APPLYING CHANGES'}`); console.log(''); // First, get refactoring analysis const analysis = await analyzeProjectRefactoring(projectPath, { targetDirs, includeNonUtility: false, includeBarrelFiles: true }); const results = { totalFiles: 0, filesChanged: 0, totalPromotions: 0, indexFilesUpdated: 0, fileResults: [], indexResults: [], errors: [] }; // Process files with promotion opportunities for (const fileAnalysis of analysis.files) { if (fileAnalysis.promotionOpportunities.length === 0) continue; results.totalFiles++; // Promote exports in this file const promotionResult = await promoteExportsInFile(fileAnalysis.file, { skipPrivate }); if (promotionResult.error) { results.errors.push(promotionResult); continue; } if (promotionResult.changed) { results.filesChanged++; results.totalPromotions += promotionResult.promoted.length; // Write changes if not dry run if (!dryRun) { await fs.promises.writeFile(promotionResult.filePath, promotionResult.newContent); } // Update index file const directory = path.dirname(promotionResult.filePath); let indexPath, indexResult; try { indexPath = await getIndexFile(directory, options); indexResult = await updateIndexFile(indexPath, promotionResult.promoted, promotionResult.filePath, { dryRun }); } catch (error) { console.error(`Error updating index file for ${directory}:`, error.message); results.errors.push({ directory, error: error.message }); continue; } if (indexResult.updates.length > 0) { results.indexFilesUpdated++; results.indexResults.push(indexResult); } results.fileResults.push(promotionResult); } } return results; } /** * Format and display results */ function displayResults(results, dryRun = false) { const action = dryRun ? 'WOULD PROMOTE' : 'PROMOTED'; if (results.totalPromotions === 0) { console.log('āœ… No utility functions found that need export promotion.'); return; } console.log(`āœ… ${action} ${results.totalPromotions} functions to exports:\n`); // Show file changes results.fileResults.forEach(fileResult => { console.log(`šŸ“ ${fileResult.filePath}`); fileResult.promoted.forEach(item => { const typeIcon = item.type === 'function' ? '⚔' : item.type === 'class' ? 'šŸ—ļø' : 'šŸ“¦'; console.log(` ${typeIcon} export ${item.declarationType} ${item.name} (line ${item.line})`); }); console.log(''); }); // Show index file updates if (results.indexResults.length > 0) { console.log(`šŸ“‹ ${dryRun ? 'WOULD UPDATE' : 'UPDATED'} ${results.indexFilesUpdated} index files:\n`); results.indexResults.forEach(indexResult => { const fileIcon = indexResult.isNewFile ? 'šŸ†•' : 'šŸ“'; console.log(`${fileIcon} ${indexResult.indexPath}`); indexResult.updates.forEach(update => { console.log(` āž• ${update}`); }); console.log(''); }); } // Show summary console.log(`šŸ“Š SUMMARY:`); console.log(` Files processed: ${results.totalFiles}`); console.log(` Files ${dryRun ? 'to be changed' : 'changed'}: ${results.filesChanged}`); console.log(` Functions ${dryRun ? 'to be promoted' : 'promoted'}: ${results.totalPromotions}`); console.log(` Index files ${dryRun ? 'to be updated' : 'updated'}: ${results.indexFilesUpdated}`); if (results.errors.length > 0) { console.log(` āŒ Errors: ${results.errors.length}`); results.errors.forEach(error => { console.log(` ${error.filePath}: ${error.error}`); }); } } /** * Parse command line arguments */ function parseArgs() { const toolOptions = { dryRun: { flags: ['--dry-run'], boolean: true, default: false, description: 'Show what would be changed without making modifications' }, targetDirs: { flags: ['--target-dirs'], parser: (value) => value.split(',').map(dir => dir.trim()), default: DEFAULT_CONFIG.targetDirs, description: 'Comma-separated list of target utility directories' }, indexFiles: { flags: ['--index-files'], parser: (value) => value.split(',').map(file => file.trim()), default: DEFAULT_CONFIG.indexFiles, description: 'Comma-separated list of index file names' }, skipPrivate: { flags: ['--skip-private'], boolean: true, default: true, description: 'Skip functions starting with underscore' }, includePrivate: { flags: ['--include-private'], boolean: true, description: 'Include private functions' } }; const { targetPath, options } = sharedParseArgs(getProcessArgs(), toolOptions); // Handle include-private flag if (options.includePrivate) options.skipPrivate = false; // Override extensions default from config if (!options.extensions) options.extensions = DEFAULT_CONFIG.validExtensions; if (options.help) { showHelp(); process.exit(0); } return { options, targetPath }; } /** * Main execution function */ async function main() { try { const { options, targetPath } = parseArgs(); try { await fs.promises.access(targetPath); } catch (error) { console.error(`āŒ Error: Path '${targetPath}' does not exist`); process.exit(1); } const results = await promoteProjectExports(targetPath, options); displayResults(results, options.dryRun); if (options.dryRun && results.totalPromotions > 0) { console.log('\nšŸ’” To apply these changes, run the command again without --dry-run'); } } catch (error) { console.error(`āŒ Error: ${error.message}`); process.exit(1); } } // Run the CLI if (require.main === module) { main().catch(error => { console.error("Fatal error:", error); process.exit(1); }); } module.exports = { main, promoteProjectExports, promoteExportsInFile, updateIndexFile };