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