smart-ast-analyzer
Version:
Advanced AST-based project analysis tool with deep complexity analysis, security scanning, and optional AI enhancement
410 lines (352 loc) • 11.7 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
class APIDocsGenerator {
constructor(options = {}) {
this.options = options;
}
async generateAPIDocumentation(analysisResults, outputPath) {
console.log('📚 Generating API documentation...');
const { endpoints = [], security = {}, deepAnalysis = {} } = analysisResults;
if (!endpoints.length) {
console.log('⚠️ No API endpoints found to document');
return null;
}
const docs = {
openapi: await this.generateOpenAPISpec(endpoints, security),
markdown: await this.generateMarkdownDocs(endpoints, security, deepAnalysis),
postman: await this.generatePostmanCollection(endpoints)
};
// Write documentation files
const docsDir = path.join(outputPath, 'api-docs');
await fs.mkdir(docsDir, { recursive: true });
await Promise.all([
this.writeFile(path.join(docsDir, 'openapi.json'), JSON.stringify(docs.openapi, null, 2)),
this.writeFile(path.join(docsDir, 'API.md'), docs.markdown),
this.writeFile(path.join(docsDir, 'postman-collection.json'), JSON.stringify(docs.postman, null, 2))
]);
console.log(`✅ API documentation generated in ${docsDir}`);
return docsDir;
}
async generateOpenAPISpec(endpoints, security) {
const paths = {};
const components = {
securitySchemes: {},
schemas: {}
};
// Group endpoints by path
const pathGroups = this.groupEndpointsByPath(endpoints);
for (const [pathPattern, pathEndpoints] of Object.entries(pathGroups)) {
paths[pathPattern] = {};
for (const endpoint of pathEndpoints) {
const method = endpoint.method.toLowerCase();
paths[pathPattern][method] = {
summary: endpoint.handler || `${endpoint.method} ${pathPattern}`,
description: this.generateEndpointDescription(endpoint),
parameters: this.extractParameters(endpoint, pathPattern),
responses: this.generateResponses(endpoint),
tags: this.generateTags(endpoint),
security: this.generateSecurityRequirements(endpoint)
};
// Add request body for POST/PUT/PATCH
if (['post', 'put', 'patch'].includes(method)) {
paths[pathPattern][method].requestBody = this.generateRequestBody(endpoint);
}
}
}
return {
openapi: '3.0.3',
info: {
title: 'API Documentation',
description: 'Generated API documentation from Smart AST Analyzer',
version: '1.0.0',
contact: {
name: 'Generated by Smart AST Analyzer'
}
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server'
}
],
paths,
components,
security: this.generateGlobalSecurity(security)
};
}
async generateMarkdownDocs(endpoints, security, deepAnalysis) {
let markdown = `# API Documentation
Generated by Smart AST Analyzer on ${new Date().toISOString()}
## Overview
This API contains **${endpoints.length} endpoints** across **${new Set(endpoints.map(e => e.path.split('/')[1])).size} resource groups**.
`;
// Security Overview
if (security.vulnerabilities?.length > 0) {
markdown += `## ⚠️ Security Issues
**${security.vulnerabilities.length} security issues found:**
`;
security.vulnerabilities.slice(0, 5).forEach(vuln => {
markdown += `- **${vuln.severity.toUpperCase()}**: ${vuln.message} (${vuln.file}:${vuln.line})\n`;
});
markdown += '\n';
}
// Group endpoints by resource
const resourceGroups = this.groupEndpointsByResource(endpoints);
for (const [resource, resourceEndpoints] of Object.entries(resourceGroups)) {
markdown += `## ${resource}\n\n`;
for (const endpoint of resourceEndpoints) {
markdown += `### ${endpoint.method} ${endpoint.path}\n\n`;
if (endpoint.description) {
markdown += `${endpoint.description}\n\n`;
}
// Parameters
const params = this.extractParameters(endpoint, endpoint.path);
if (params.length > 0) {
markdown += `#### Parameters\n\n`;
markdown += `| Name | Type | Location | Required | Description |\n`;
markdown += `|------|------|----------|----------|-------------|\n`;
params.forEach(param => {
markdown += `| ${param.name} | ${param.schema?.type || 'string'} | ${param.in} | ${param.required ? 'Yes' : 'No'} | ${param.description || '-'} |\n`;
});
markdown += '\n';
}
// Example Request
markdown += `#### Example Request\n\n`;
markdown += `\`\`\`bash\n`;
markdown += `curl -X ${endpoint.method} \\\n`;
markdown += ` "${endpoint.path}" \\\n`;
if (endpoint.auth) {
markdown += ` -H "Authorization: Bearer YOUR_TOKEN" \\\n`;
}
markdown += ` -H "Content-Type: application/json"\n`;
markdown += `\`\`\`\n\n`;
// Security notes
if (!endpoint.auth) {
markdown += `> ⚠️ **Security Warning**: This endpoint has no authentication\n\n`;
}
markdown += `---\n\n`;
}
}
// Complexity Analysis
if (deepAnalysis.complexity) {
markdown += `## Code Complexity Analysis\n\n`;
markdown += `**Overall Complexity Rating**: ${deepAnalysis.complexity.overall.rating}\n\n`;
if (deepAnalysis.complexity.functions?.length > 0) {
markdown += `### Complex Functions\n\n`;
deepAnalysis.complexity.functions
.slice(0, 10)
.forEach(func => {
markdown += `- **${func.name}** (${func.file}:${func.line})\n`;
markdown += ` - Cyclomatic: ${func.cyclomatic}\n`;
markdown += ` - Cognitive: ${func.cognitive}\n`;
if (func.warnings?.length > 0) {
markdown += ` - ⚠️ ${func.warnings.join(', ')}\n`;
}
markdown += `\n`;
});
}
}
return markdown;
}
async generatePostmanCollection(endpoints) {
const items = endpoints.map(endpoint => ({
name: `${endpoint.method} ${endpoint.path}`,
request: {
method: endpoint.method,
header: [
{
key: 'Content-Type',
value: 'application/json',
type: 'text'
}
].concat(endpoint.auth ? [{
key: 'Authorization',
value: 'Bearer {{token}}',
type: 'text'
}] : []),
url: {
raw: `{{baseUrl}}${endpoint.path}`,
host: ['{{baseUrl}}'],
path: endpoint.path.split('/').filter(p => p)
},
description: endpoint.description || `${endpoint.method} request to ${endpoint.path}`
},
response: []
}));
return {
info: {
name: 'API Collection',
description: 'Generated by Smart AST Analyzer',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
},
variable: [
{
key: 'baseUrl',
value: 'http://localhost:3000',
type: 'string'
},
{
key: 'token',
value: 'your-auth-token',
type: 'string'
}
],
item: items
};
}
groupEndpointsByPath(endpoints) {
const groups = {};
endpoints.forEach(endpoint => {
if (!groups[endpoint.path]) {
groups[endpoint.path] = [];
}
groups[endpoint.path].push(endpoint);
});
return groups;
}
groupEndpointsByResource(endpoints) {
const groups = {};
endpoints.forEach(endpoint => {
const resource = this.extractResource(endpoint.path);
if (!groups[resource]) {
groups[resource] = [];
}
groups[resource].push(endpoint);
});
return groups;
}
extractResource(path) {
const parts = path.split('/').filter(p => p && !p.startsWith(':'));
if (parts.length === 0) return 'Root';
if (parts[0] === 'api' && parts.length > 1) {
return parts[1].charAt(0).toUpperCase() + parts[1].slice(1);
}
return parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
}
extractParameters(endpoint, pathPattern) {
const params = [];
// Path parameters
const pathParams = pathPattern.match(/:(\w+)/g);
if (pathParams) {
pathParams.forEach(param => {
params.push({
name: param.substring(1),
in: 'path',
required: true,
schema: { type: 'string' },
description: `${param.substring(1)} identifier`
});
});
}
// Query parameters (basic detection)
if (endpoint.method === 'GET') {
params.push({
name: 'limit',
in: 'query',
required: false,
schema: { type: 'integer', minimum: 1, maximum: 100 },
description: 'Number of items to return'
});
params.push({
name: 'offset',
in: 'query',
required: false,
schema: { type: 'integer', minimum: 0 },
description: 'Number of items to skip'
});
}
return params;
}
generateEndpointDescription(endpoint) {
const resourceName = this.extractResource(endpoint.path);
const action = this.getActionFromMethod(endpoint.method);
return `${action} ${resourceName.toLowerCase()} resource`;
}
getActionFromMethod(method) {
const actions = {
'GET': 'Retrieve',
'POST': 'Create',
'PUT': 'Update',
'PATCH': 'Partially update',
'DELETE': 'Delete'
};
return actions[method.toUpperCase()] || 'Process';
}
generateResponses(endpoint) {
const responses = {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' }
}
}
}
}
}
};
if (endpoint.method === 'POST') {
responses['201'] = {
description: 'Resource created successfully',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
id: { type: 'string' }
}
}
}
}
};
}
if (!endpoint.auth) {
responses['401'] = {
description: 'Unauthorized - Authentication required'
};
}
responses['500'] = {
description: 'Internal server error'
};
return responses;
}
generateRequestBody(endpoint) {
return {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
// Basic schema based on resource
data: { type: 'object' }
}
}
}
}
};
}
generateTags(endpoint) {
return [this.extractResource(endpoint.path)];
}
generateSecurityRequirements(endpoint) {
return endpoint.auth ? [{ BearerAuth: [] }] : [];
}
generateGlobalSecurity(security) {
return security.authRequired ? [{ BearerAuth: [] }] : [];
}
async writeFile(filePath, content) {
try {
await fs.writeFile(filePath, content, 'utf-8');
} catch (error) {
console.warn(`Failed to write ${filePath}:`, error.message);
}
}
}
module.exports = APIDocsGenerator;