UNPKG

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