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
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.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