sca-tool
Version:
Universal static code analysis tool for calculating API operation costs across multiple frameworks (NestJS, Express, Fastify)
324 lines (319 loc) • 15.2 kB
JavaScript
;
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.ReportGenerator = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Report generator for API cost analysis
*/
class ReportGenerator {
/**
* Generate report in specified format
*/
static generateReport(analysis, format = 'text') {
switch (format) {
case 'json':
return this.generateJsonReport(analysis);
case 'markdown':
return this.generateMarkdownReport(analysis);
case 'html':
return this.generateHtmlReport(analysis);
case 'text':
default:
return this.generateTextReport(analysis);
}
}
/**
* Generate project report for multiple files
*/
static generateProjectReport(files, format = 'text') {
const summary = this.calculateProjectSummary(files);
switch (format) {
case 'json':
return JSON.stringify({ files, summary }, null, 2);
case 'markdown':
return this.generateProjectMarkdownReport(files, summary);
case 'html':
return this.generateProjectHtmlReport(files, summary);
case 'text':
default:
return this.generateProjectTextReport(files, summary);
}
}
static generateTextReport(analysis) {
let report = '\n' + '='.repeat(80) + '\n';
report += ' 🔍 API COST ANALYSIS REPORT\n';
report += '='.repeat(80) + '\n';
report += `📁 File: ${analysis.fileName}\n`;
report += `📅 Generated: ${new Date().toLocaleString()}\n\n`;
// Summary
report += '📊 SUMMARY\n';
report += '========================================\n';
report += `Methods Analyzed: ${analysis.summary.totalMethods}\n`;
report += `Total Execution Paths: ${analysis.summary.totalPaths}\n`;
report += `Cost Range: ${analysis.summary.costRange.min} - ${analysis.summary.costRange.max} requests\n`;
report += `Average Cost: ${analysis.summary.costRange.average} requests\n\n`;
// Methods
analysis.methods.forEach((method, index) => {
report += `📋 METHOD ${index + 1}: ${method.className ? method.className + '.' : ''}${method.methodName}\n`;
report += '----------------------------------------\n';
report += `Total Paths: ${method.totalPaths}\n`;
report += `Cost Range: ${method.summary.minCost} - ${method.summary.maxCost} requests\n\n`;
method.paths.forEach((path, pathIndex) => {
report += ` 🌳 Path ${pathIndex + 1}: ${path.name}\n`;
report += ` Cost: ${path.totalCost} requests\n`;
if (path.conditions.length > 0) {
report += ` Conditions: ${path.conditions.join(', ')}\n`;
}
report += ` Operations:\n`;
path.operations.forEach(op => {
report += ` Line ${op.line}: ${op.method} (Cost: ${op.cost})\n`;
});
report += '\n';
});
if (method.recommendations.length > 0) {
report += ' 🎯 Recommendations:\n';
method.recommendations.forEach(rec => {
report += ` • ${rec}\n`;
});
report += '\n';
}
});
return report;
}
static generateJsonReport(analysis) {
return JSON.stringify(analysis, null, 2);
}
static generateMarkdownReport(analysis) {
let md = `# 🔍 API Cost Analysis Report\n\n`;
md += `**File:** ${analysis.fileName}\n`;
md += `**Generated:** ${new Date().toLocaleString()}\n\n`;
// Summary
md += `## 📊 Summary\n\n`;
md += `| Metric | Value |\n`;
md += `|--------|-------|\n`;
md += `| Methods Analyzed | ${analysis.summary.totalMethods} |\n`;
md += `| Total Execution Paths | ${analysis.summary.totalPaths} |\n`;
md += `| Cost Range | ${analysis.summary.costRange.min} - ${analysis.summary.costRange.max} requests |\n`;
md += `| Average Cost | ${analysis.summary.costRange.average} requests |\n\n`;
// Methods
md += `## 📋 Methods Analysis\n\n`;
analysis.methods.forEach((method, index) => {
const methodName = method.className ? `${method.className}.${method.methodName}` : method.methodName;
md += `### ${index + 1}. ${methodName}\n\n`;
md += `- **Total Paths:** ${method.totalPaths}\n`;
md += `- **Cost Range:** ${method.summary.minCost} - ${method.summary.maxCost} requests\n\n`;
md += `#### 🌳 Execution Paths\n\n`;
method.paths.forEach((path, pathIndex) => {
md += `**${pathIndex + 1}. ${path.name}** (Cost: ${path.totalCost})\n\n`;
if (path.conditions.length > 0) {
md += `*Conditions:* ${path.conditions.join(', ')}\n\n`;
}
md += `Operations:\n`;
path.operations.forEach(op => {
md += `- Line ${op.line}: \`${op.method}\` (Cost: ${op.cost})\n`;
});
md += '\n';
});
if (method.recommendations.length > 0) {
md += `#### 🎯 Recommendations\n\n`;
method.recommendations.forEach(rec => {
md += `- ${rec}\n`;
});
md += '\n';
}
});
return md;
}
static generateHtmlReport(analysis) {
const methodsHtml = analysis.methods.map(method => {
const methodName = method.className ? `${method.className}.${method.methodName}` : method.methodName;
const costClass = method.summary.maxCost > 5 ? 'cost-high' : method.summary.maxCost > 2 ? 'cost-medium' : 'cost-low';
const pathsHtml = method.paths.map((path, index) => {
const operationsHtml = path.operations.map(op => `<div class="operation">Line ${op.line}: ${op.method} (Cost: ${op.cost})</div>`).join('');
return `
<div class="path">
<strong>${index + 1}. ${path.name}</strong> (Cost: ${path.totalCost})
${path.conditions.length > 0 ? `<br><em>Conditions: ${path.conditions.join(', ')}</em>` : ''}
<div style="margin-top: 10px;">
${operationsHtml}
</div>
</div>
`;
}).join('');
const recommendationsHtml = method.recommendations.length > 0 ? `
<div class="recommendations">
<h4>🎯 Recommendations</h4>
<ul>
${method.recommendations.map(rec => `<li>${rec}</li>`).join('')}
</ul>
</div>
` : '';
return `
<div class="method">
<h3>${methodName}</h3>
<p><strong>Paths:</strong> ${method.totalPaths} | <strong>Cost:</strong> <span class="${costClass}">${method.summary.minCost} - ${method.summary.maxCost}</span></p>
<h4>🌳 Execution Paths</h4>
${pathsHtml}
${recommendationsHtml}
</div>
`;
}).join('');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Cost Analysis - ${analysis.fileName}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 20px 0; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.method { background: white; padding: 20px; margin: 20px 0; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.path { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 8px; border-left: 4px solid #007bff; }
.operation { background: #e9ecef; padding: 10px; margin: 5px 0; border-radius: 6px; font-family: 'Monaco', 'Menlo', monospace; font-size: 14px; }
.recommendations { background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745; margin-top: 15px; }
h1 { color: #333; margin: 0; font-size: 2.5em; }
h2 { color: #555; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
h3 { color: #666; }
.cost-high { color: #dc3545; font-weight: bold; }
.cost-medium { color: #ffc107; font-weight: bold; }
.cost-low { color: #28a745; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 API Cost Analysis Report</h1>
<p><strong>File:</strong> ${analysis.fileName}</p>
<p><strong>Generated:</strong> ${new Date().toLocaleString()}</p>
</div>
<div class="summary">
<div class="card">
<h3>📋 Methods</h3>
<div style="font-size: 2em; font-weight: bold; color: #007bff;">${analysis.summary.totalMethods}</div>
</div>
<div class="card">
<h3>🌳 Execution Paths</h3>
<div style="font-size: 2em; font-weight: bold; color: #28a745;">${analysis.summary.totalPaths}</div>
</div>
<div class="card">
<h3>💰 Cost Range</h3>
<div style="font-size: 1.5em; font-weight: bold; color: #ffc107;">${analysis.summary.costRange.min} - ${analysis.summary.costRange.max}</div>
</div>
<div class="card">
<h3>📊 Average Cost</h3>
<div style="font-size: 1.5em; font-weight: bold; color: #6f42c1;">${analysis.summary.costRange.average}</div>
</div>
</div>
<h2>📋 Methods Analysis</h2>
${methodsHtml}
<div style="text-align: center; margin-top: 40px; color: #666; padding: 20px;">
Generated by <strong>SCA-Tool</strong> - Static Code Analyzer for API Cost Analysis
</div>
</div>
</body>
</html>`;
}
static generateProjectTextReport(files, summary) {
let report = '\n' + '='.repeat(80) + '\n';
report += ' 🔍 PROJECT API COST ANALYSIS REPORT\n';
report += '='.repeat(80) + '\n';
report += `📅 Generated: ${new Date().toLocaleString()}\n\n`;
// Project summary
report += '📊 PROJECT SUMMARY\n';
report += '========================================\n';
report += `Files Analyzed: ${files.length}\n`;
report += `Methods Analyzed: ${summary.totalMethods}\n`;
report += `Total Execution Paths: ${summary.totalPaths}\n\n`;
// File breakdown
report += '📁 FILE BREAKDOWN\n';
report += '========================================\n';
files.forEach(file => {
const status = file.summary.costRange.max <= 2 ? '✅' : '⚠️';
report += `${status} ${file.fileName}: ${file.summary.totalMethods} methods, ${file.summary.totalPaths} paths, cost range: ${file.summary.costRange.min}-${file.summary.costRange.max}\n`;
});
return report;
}
static generateProjectMarkdownReport(files, summary) {
let md = '# 🔍 Project API Cost Analysis Report\n\n';
md += `**Generated:** ${new Date().toLocaleString()}\n\n`;
md += '## 📊 Project Summary\n\n';
md += `- **Files Analyzed:** ${files.length}\n`;
md += `- **Methods Analyzed:** ${summary.totalMethods}\n`;
md += `- **Total Execution Paths:** ${summary.totalPaths}\n\n`;
md += '## 📁 File Breakdown\n\n';
files.forEach(file => {
const status = file.summary.costRange.max <= 2 ? '✅' : '⚠️';
md += `${status} **${file.fileName}:** ${file.summary.totalMethods} methods, ${file.summary.totalPaths} paths, cost range: ${file.summary.costRange.min}-${file.summary.costRange.max}\n`;
});
return md;
}
static generateProjectHtmlReport(files, summary) {
// Simplified project HTML report
return this.generateHtmlReport(files[0]); // For now, just show first file
}
static calculateProjectSummary(files) {
const allMethods = files.flatMap(f => f.methods);
return {
totalMethods: allMethods.length,
totalPaths: allMethods.reduce((sum, m) => sum + m.totalPaths, 0)
};
}
/**
* Save report to file
*/
static saveReport(content, filePath) {
try {
const dir = path.dirname(filePath);
// Create directory if it doesn't exist
if (dir && dir !== '.' && dir !== filePath) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
fs.writeFileSync(filePath, content, 'utf8');
console.log(`✅ Report saved to: ${filePath}`);
}
catch (error) {
throw new Error(`Failed to save report to ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
exports.ReportGenerator = ReportGenerator;
//# sourceMappingURL=reports.js.map