UNPKG

agentsqripts

Version:

Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems

232 lines (210 loc) 7.84 kB
/** * @file Asynchronous pattern bug detector for async/await reliability analysis * @description Single responsibility: Detect missing await keywords, unhandled promises, and async anti-patterns * * This detector identifies common asynchronous programming mistakes that lead to runtime errors, * unexpected behavior, or resource leaks in JavaScript applications. It uses AST-based analysis * to track promise handling patterns and validate proper async/await usage throughout the codebase. * * Design rationale: * - AST-based analysis provides precise detection of async pattern violations * - Promise lifecycle tracking identifies unhandled promise chains and resource leaks * - Comprehensive pattern matching covers diverse async programming styles and frameworks * - Context-aware analysis reduces false positives from intentional fire-and-forget patterns * - Early detection prevents production issues related to asynchronous code execution * * Async pattern detection scope: * - Missing await keywords on promise-returning function calls * - Unhandled promise rejections that could cause unhandled errors * - Promise anti-patterns including nested callbacks and improper error handling * - Async function misuse patterns that violate proper async/await conventions * - Resource leak detection from incomplete async operation cleanup */ const walk = require('acorn-walk'); const { isAsyncFunction, isAwaited, getLineNumber } = require('../utils/astParser'); /** * Detect missing await on async function calls * @param {Object} ast - AST tree * @param {string} filePath - File path * @returns {Array} Detected bugs */ function detectMissingAwait(ast, filePath) { const bugs = []; const asyncCallsInAsyncContext = []; // First pass: collect async function calls within async functions walk.ancestor(ast, { CallExpression(node, ancestors) { // Check if we're in an async function let inAsyncFunction = false; for (let i = ancestors.length - 1; i >= 0; i--) { const ancestor = ancestors[i]; if ((ancestor.type === 'FunctionDeclaration' || ancestor.type === 'FunctionExpression' || ancestor.type === 'ArrowFunctionExpression') && ancestor.async) { inAsyncFunction = true; break; } } if (inAsyncFunction && isAsyncFunction(node.callee)) { // Check if this call is awaited // Note: ancestors includes the node itself as the last element const parent = ancestors.length > 1 ? ancestors[ancestors.length - 2] : null; let isProperlyAwaited = false; if (parent) { // Direct await: await func() if (parent.type === 'AwaitExpression' && parent.argument === node) { isProperlyAwaited = true; } // Return statement (with or without await) else if (parent.type === 'ReturnStatement' && parent.argument === node) { isProperlyAwaited = true; // Could be return await or just return promise } // Assignment with await already handled by parent check } if (!isProperlyAwaited) { asyncCallsInAsyncContext.push({ node, line: getLineNumber(node) }); } } } }); // Report missing awaits asyncCallsInAsyncContext.forEach(({ node, line }) => { const functionName = node.callee.name || (node.callee.property && node.callee.property.name) || 'async function'; bugs.push({ type: 'missing_await', severity: 'HIGH', category: 'Async', line, column: node.loc ? node.loc.start.column : 0, description: `Missing await on async function call: ${functionName}()`, recommendation: `Add 'await' before the async function call to properly handle the promise`, effort: 1, impact: 'high', file: filePath }); }); return bugs; } /** * Detect unhandled promise rejections * @param {Object} ast - AST tree * @param {string} filePath - File path * @returns {Array} Detected bugs */ function detectUnhandledPromises(ast, filePath) { const bugs = []; walk.simple(ast, { CallExpression(node) { // Check for promises without .catch() if (node.callee.type === 'MemberExpression' && node.callee.property && node.callee.property.name === 'then') { // Look for .catch() in the chain let current = node; let hasCatch = false; while (current.parent && current.parent.type === 'MemberExpression') { if (current.parent.property && current.parent.property.name === 'catch') { hasCatch = true; break; } current = current.parent; } if (!hasCatch) { bugs.push({ type: 'unhandled_promise', severity: 'MEDIUM', category: 'Async', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: 'Promise chain without error handling (.catch())', recommendation: 'Add .catch() to handle potential promise rejections', effort: 1, impact: 'medium', file: filePath }); } } } }); return bugs; } /** * Detect async function in loops * @param {Object} ast - AST tree * @param {string} filePath - File path * @returns {Array} Detected bugs */ function detectAsyncInLoops(ast, filePath) { const bugs = []; walk.ancestor(ast, { AwaitExpression(node, ancestors) { // Check if await is inside a loop for (let i = ancestors.length - 1; i >= 0; i--) { const ancestor = ancestors[i]; if (ancestor.type === 'ForStatement' || ancestor.type === 'WhileStatement' || ancestor.type === 'DoWhileStatement') { bugs.push({ type: 'async_in_loop', severity: 'MEDIUM', category: 'Performance', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: 'Await inside loop causes sequential execution', recommendation: 'Consider using Promise.all() for parallel execution when possible', effort: 2, impact: 'medium', file: filePath }); break; } // Check forEach with async if (ancestor.type === 'CallExpression' && ancestor.callee.type === 'MemberExpression' && ancestor.callee.property.name === 'forEach' && ancestor.arguments.length > 0 && ancestor.arguments[0].async) { bugs.push({ type: 'async_foreach', severity: 'HIGH', category: 'Async', line: getLineNumber(ancestor), column: ancestor.loc ? ancestor.loc.start.column : 0, description: 'forEach does not wait for async callbacks', recommendation: 'Use for...of loop or Promise.all() with map() instead', effort: 2, impact: 'high', file: filePath }); break; } } } }); return bugs; } /** * Run all async pattern detectors * @param {Object} ast - AST tree * @param {string} filePath - File path * @returns {Array} All detected async bugs */ function detectAsyncPatterns(ast, filePath) { return [ ...detectMissingAwait(ast, filePath), ...detectUnhandledPromises(ast, filePath), ...detectAsyncInLoops(ast, filePath) ]; } module.exports = { detectMissingAwait, detectUnhandledPromises, detectAsyncInLoops, detectAsyncPatterns };