UNPKG

agentsqripts

Version:

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

181 lines (165 loc) 5.8 kB
/** * @file Resource leak detector for memory and system resource management validation * @description Single responsibility: Detect potential memory leaks and resource management issues * * This detector identifies patterns that commonly lead to memory leaks and resource exhaustion * in JavaScript applications. It analyzes resource allocation patterns, cleanup procedures, * and lifecycle management to ensure proper resource disposal and prevent performance * degradation from accumulated memory leaks. * * Design rationale: * - Pattern-based detection identifies common resource leak scenarios efficiently * - Lifecycle tracking validates resource allocation and cleanup pairing * - Event listener analysis prevents DOM memory leaks in browser environments * - Timer resource tracking identifies abandoned intervals and timeouts * - Context-aware analysis reduces false positives from intentional resource retention * * Resource leak detection scope: * - Event listener registration without corresponding removal handlers * - Timer creation (setTimeout, setInterval) without proper cleanup * - DOM element references that prevent garbage collection * - Closure-based memory leaks from retained scope references * - Stream and file handle management without proper disposal */ const walk = require('acorn-walk'); const { getLineNumber } = require('../utils/astParser'); /** * Detect potential resource leaks * @param {Object} ast - AST tree * @param {string} filePath - File path * @returns {Array} Detected bugs */ function detectResourceLeaks(ast, filePath) { const bugs = []; const resources = trackResources(ast); // Check for unreleased resources resources.timers.forEach(timer => { if (!timer.cleared) { bugs.push({ type: 'uncleaned_timer', severity: 'MEDIUM', category: 'Memory Leak', line: timer.line, column: 0, description: `Timer (${timer.type}) may not be cleared`, recommendation: `Store timer ID and call ${timer.type === 'setInterval' ? 'clearInterval' : 'clearTimeout'} when no longer needed`, effort: 1, impact: 'medium', file: filePath }); } }); resources.listeners.forEach(listener => { if (!listener.removed) { bugs.push({ type: 'uncleaned_listener', severity: 'MEDIUM', category: 'Memory Leak', line: listener.line, column: 0, description: `Event listener for '${listener.event}' may not be removed`, recommendation: 'Remove event listener when component unmounts or is no longer needed', effort: 1, impact: 'medium', file: filePath }); } }); return bugs; } /** * Track resource allocation and cleanup * @param {Object} ast - AST tree * @returns {Object} Resource tracking info */ function trackResources(ast) { const resources = { timers: [], listeners: [], subscriptions: [] }; const timerIds = new Set(); const listenerRefs = new Set(); walk.simple(ast, { CallExpression(node) { const calleeName = getCalleeName(node); // Track timer creation if (calleeName === 'setTimeout' || calleeName === 'setInterval') { const timerId = getAssignedVariable(node); resources.timers.push({ type: calleeName, line: getLineNumber(node), id: timerId, cleared: false }); if (timerId) timerIds.add(timerId); } // Track timer cleanup if (calleeName === 'clearTimeout' || calleeName === 'clearInterval') { const arg = node.arguments[0]; if (arg && arg.type === 'Identifier' && timerIds.has(arg.name)) { // Mark timer as cleared resources.timers.forEach(timer => { if (timer.id === arg.name) timer.cleared = true; }); } } // Track event listeners if (calleeName === 'addEventListener' || (node.callee.type === 'MemberExpression' && node.callee.property.name === 'addEventListener')) { const event = node.arguments[0]; const handler = node.arguments[1]; if (event && event.type === 'Literal') { const ref = handler && handler.type === 'Identifier' ? handler.name : null; resources.listeners.push({ event: event.value, line: getLineNumber(node), handler: ref, removed: false }); if (ref) listenerRefs.add(ref); } } // Track listener removal if (calleeName === 'removeEventListener' || (node.callee.type === 'MemberExpression' && node.callee.property.name === 'removeEventListener')) { const event = node.arguments[0]; const handler = node.arguments[1]; if (event && event.type === 'Literal' && handler && handler.type === 'Identifier') { // Mark listener as removed resources.listeners.forEach(listener => { if (listener.event === event.value && listener.handler === handler.name) { listener.removed = true; } }); } } } }); return resources; } /** * Get the name of the function being called */ function getCalleeName(node) { if (node.callee.type === 'Identifier') { return node.callee.name; } if (node.callee.type === 'MemberExpression' && node.callee.property) { return node.callee.property.name; } return null; } /** * Get the variable a function result is assigned to */ function getAssignedVariable(node) { // This would need to check parent nodes for assignment // Simplified for now return null; } module.exports = { detectResourceLeaks };