UNPKG

static-code-analyzer

Version:

Universal static code analysis tool for calculating API operation costs across multiple frameworks (NestJS, Express, Fastify)

346 lines 13.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiCostAnalyzer = void 0; exports.analyzeNestJSFile = analyzeNestJSFile; exports.analyzeNestJSProject = analyzeNestJSProject; exports.analyzeExpressProject = analyzeExpressProject; const ts = __importStar(require("typescript")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const glob = __importStar(require("glob")); const config_1 = require("./config"); /** * Main API Cost Analyzer class */ class ApiCostAnalyzer { config; allPaths = []; currentConditions = []; sourceFile; constructor(config = {}) { this.config = (0, config_1.mergeConfigs)(config_1.DEFAULT_CONFIG, config); } /** * Set framework preset */ setFramework(framework) { const frameworkConfig = (0, config_1.getFrameworkConfig)(framework); if (frameworkConfig) { this.config = (0, config_1.mergeConfigs)(this.config, frameworkConfig); } } /** * Update configuration */ updateConfig(config) { this.config = (0, config_1.mergeConfigs)(this.config, config); } /** * Analyze a single file */ analyzeFile(filePath) { if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } const sourceCode = fs.readFileSync(filePath, 'utf8'); this.sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true); const methods = []; this.visit(this.sourceFile, (node) => { if (this.isMethodDeclaration(node) || this.isArrowFunctionProperty(node)) { const methodName = this.getMethodName(node); const className = this.getClassName(node); if (methodName && this.shouldAnalyzeMethod(methodName)) { const analysis = this.analyzeMethod(node, methodName, filePath); if (analysis.paths.length > 0) { analysis.className = className; methods.push(analysis); } } } }); return { filePath, fileName: path.basename(filePath), methods, summary: this.generateFileSummary(methods) }; } /** * Analyze multiple files */ analyzeFiles(filePaths) { return filePaths.map(filePath => this.analyzeFile(filePath)); } /** * Analyze entire project */ analyzeProject(projectPath, patterns = ['**/*.ts', '**/*.js']) { const files = []; patterns.forEach(pattern => { const matches = glob.sync(path.join(projectPath, pattern)); files.push(...matches.filter(file => !file.includes('.spec.') && !file.includes('.test.') && !file.includes('.d.ts') && fs.statSync(file).isFile())); }); const uniqueFiles = [...new Set(files)]; const fileAnalyses = this.analyzeFiles(uniqueFiles); return { projectPath, files: fileAnalyses, summary: this.generateProjectSummary(fileAnalyses, projectPath), generatedAt: new Date().toISOString() }; } generatePathDescription(conditions, operations) { const conditionDesc = conditions.length > 0 ? `When ${conditions.join(' and ')}` : 'Default execution'; const operationDesc = operations.length > 0 ? `performs ${operations.length} operation(s)` : 'no operations'; return `${conditionDesc}, ${operationDesc}`; } generateMethodSummary() { const costs = this.allPaths.map(p => p.totalCost); const minCost = Math.min(...costs); const maxCost = Math.max(...costs); const avgCost = Math.round((costs.reduce((a, b) => a + b, 0) / costs.length) * 100) / 100; // Find most common path (lowest cost as default) const commonPath = this.allPaths.find(p => p.totalCost === minCost) || this.allPaths[0]; return { minCost, maxCost, avgCost, commonPath }; } analyzeMethod(node, methodName, filePath) { this.allPaths = []; this.currentConditions = []; // Analyze the method body this.analyzeNode(node, []); if (this.allPaths.length === 0) { // Create default path if no operations found this.allPaths.push({ name: 'Default Path', conditions: [], operations: [], totalCost: 0, description: 'No database operations found', parallelGroups: [] }); } const summary = this.generateMethodSummary(); const recommendations = this.generateRecommendations(); return { methodName, filePath, totalPaths: this.allPaths.length, paths: this.allPaths, summary, recommendations }; } analyzeNode(node, conditions) { // Implementation for analyzing AST nodes // This is a simplified version - full implementation would handle all TypeScript constructs ts.forEachChild(node, (child) => { this.analyzeNode(child, conditions); }); } generateRecommendations() { const recommendations = []; const summary = this.generateMethodSummary(); // High cost variance if (summary.maxCost > summary.minCost * 1.5) { recommendations.push(`⚠️ High cost variance (${summary.minCost}-${summary.maxCost}) - Consider optimizing expensive paths`); } // High maximum cost if (summary.maxCost > 5) { recommendations.push(`⚠️ High maximum cost (${summary.maxCost}) - Consider caching or batching operations`); } // Sequential operations that could be parallel const hasSequentialOps = this.allPaths.some(path => path.operations.length > 1 && path.operations.every(op => !op.isParallel)); if (hasSequentialOps) { recommendations.push(`💡 Consider using Promise.all() for independent operations`); } if (recommendations.length === 0) { recommendations.push(`✅ Cost structure looks optimal`); } return recommendations; } generateFileSummary(methods) { const allCosts = methods.flatMap(m => m.paths.map(p => p.totalCost)); const totalPaths = methods.reduce((sum, m) => sum + m.totalPaths, 0); return { totalMethods: methods.length, totalPaths, costRange: { min: allCosts.length > 0 ? Math.min(...allCosts) : 0, max: allCosts.length > 0 ? Math.max(...allCosts) : 0, average: allCosts.length > 0 ? Math.round((allCosts.reduce((a, b) => a + b, 0) / allCosts.length) * 100) / 100 : 0 } }; } generateProjectSummary(files, projectPath) { const allMethods = files.flatMap(f => f.methods); const allCosts = allMethods.flatMap(m => m.paths.map(p => p.totalCost)); const totalPaths = allMethods.reduce((sum, m) => sum + m.totalPaths, 0); // Categorize costs const lowCost = allCosts.filter(c => c <= 2).length; const mediumCost = allCosts.filter(c => c > 2 && c <= 5).length; const highCost = allCosts.filter(c => c > 5).length; // Generate optimization opportunities const optimizationOpportunities = []; allMethods.forEach(method => { const variance = method.summary.maxCost - method.summary.minCost; if (variance > 2) { optimizationOpportunities.push({ file: method.filePath, method: method.methodName, type: 'high_variance', description: `Cost variance: ${method.summary.minCost}-${method.summary.maxCost}`, priority: variance > 5 ? 'high' : 'medium' }); } if (method.summary.maxCost > 5) { optimizationOpportunities.push({ file: method.filePath, method: method.methodName, type: 'high_cost', description: `High maximum cost: ${method.summary.maxCost}`, priority: 'high', suggestion: 'Consider caching, batching, or breaking into smaller operations' }); } }); return { totalFiles: files.length, totalMethods: allMethods.length, totalPaths, costDistribution: { low: lowCost, medium: mediumCost, high: highCost }, optimizationOpportunities, generatedAt: new Date().toISOString() }; } // Helper methods for AST traversal visit(node, callback) { callback(node); ts.forEachChild(node, (child) => this.visit(child, callback)); } isMethodDeclaration(node) { return ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node); } isArrowFunctionProperty(node) { return ts.isPropertyAssignment(node) && node.initializer && ts.isArrowFunction(node.initializer); } getMethodName(node) { if (ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node)) { return node.name?.getText(this.sourceFile) || null; } if (ts.isPropertyAssignment(node)) { return node.name?.getText(this.sourceFile) || null; } return null; } getClassName(node) { let parent = node.parent; while (parent) { if (ts.isClassDeclaration(parent)) { return parent.name?.getText(this.sourceFile); } parent = parent.parent; } return undefined; } shouldAnalyzeMethod(methodName) { // Skip certain methods const skipPatterns = [ 'constructor', 'ngOnInit', 'ngOnDestroy', 'toString', 'valueOf', 'hasOwnProperty' ]; return !skipPatterns.some(pattern => methodName.includes(pattern)) && !methodName.startsWith('_') && !methodName.startsWith('private'); } } exports.ApiCostAnalyzer = ApiCostAnalyzer; /** * Convenience function to quickly analyze a file with NestJS preset */ function analyzeNestJSFile(filePath, customConfig) { const analyzer = new ApiCostAnalyzer(); analyzer.setFramework('nestjs'); if (customConfig) { analyzer.updateConfig(customConfig); } return analyzer.analyzeFile(filePath); } /** * Convenience function to quickly analyze a NestJS project */ function analyzeNestJSProject(projectPath, patterns, customConfig) { const analyzer = new ApiCostAnalyzer(); analyzer.setFramework('nestjs'); if (customConfig) { analyzer.updateConfig(customConfig); } return analyzer.analyzeProject(projectPath, patterns || ['**/*.service.ts', '**/*.controller.ts', '**/*.gateway.ts']); } /** * Convenience function for Express projects */ function analyzeExpressProject(projectPath, patterns, customConfig) { const analyzer = new ApiCostAnalyzer(); analyzer.setFramework('express'); if (customConfig) { analyzer.updateConfig(customConfig); } return analyzer.analyzeProject(projectPath, patterns || ['**/*.js', '**/*.ts', '**/routes/**/*.js', '**/controllers/**/*.js']); } //# sourceMappingURL=analyzer.js.map