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
JavaScript
/**
* @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
};