agentsqripts
Version:
Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems
345 lines (313 loc) • 12.3 kB
JavaScript
/**
* @file Use-before-initialization detector for variable lifecycle validation
* @description Single responsibility: Detect variables accessed before proper initialization
*
* This detector identifies cases where variables are accessed before they have been properly
* initialized, which can lead to ReferenceError exceptions or undefined behavior in JavaScript
* applications. It performs control flow analysis to track variable declarations and usage
* patterns across different execution paths.
*
* Design rationale:
* - Control flow analysis tracks variable lifecycle across all possible execution paths
* - Scope-aware detection handles block scoping, function scoping, and hoisting correctly
* - Global variable filtering prevents false positives from built-in JavaScript objects
* - Declaration pattern recognition covers var, let, const, and function declarations
* - Path-sensitive analysis ensures accurate detection in complex conditional logic
*
* Use-before-initialization detection scope:
* - Variable access before declaration in the same scope
* - Temporal dead zone violations for let and const declarations
* - Function hoisting edge cases that could cause runtime errors
* - Conditional initialization patterns that might leave variables undefined
* - Cross-scope variable access patterns that bypass proper initialization
*/
const walk = require('acorn-walk');
const { getLineNumber } = require('../../../utils/astParser');
const isGlobalVariable = require('../check/isGlobalVariable');
const isBuiltIn = require('../check/isBuiltIn');
/**
* Detect use before initialization
* @param {Object} ast - AST tree
* @param {string} filePath - File path
* @returns {Array} Detected bugs
*/
function detectUseBeforeInit(ast, filePath) {
const bugs = [];
const scopes = [];
let currentScope = null;
// Enter a new scope
function enterScope() {
const newScope = {
parent: currentScope,
declarations: new Map()
};
scopes.push(newScope);
currentScope = newScope;
}
// Exit current scope
function exitScope() {
scopes.pop();
currentScope = scopes.length > 0 ? scopes[scopes.length - 1] : null;
}
// Find declaration in scope chain
function findDeclaration(name) {
let scope = currentScope;
while (scope) {
if (scope.declarations.has(name)) {
return scope.declarations.get(name);
}
scope = scope.parent;
}
return null;
}
// Add declaration to current scope
function addDeclaration(name, line, hoisted = false) {
if (currentScope) {
currentScope.declarations.set(name, { line: hoisted ? 0 : line });
}
}
// Initialize with global scope
enterScope();
// Pre-collect hoisted function and class declarations
walk.simple(ast, {
FunctionDeclaration(node) {
if (node.id) {
// Functions are hoisted to their containing scope
addDeclaration(node.id.name, 0, true);
}
},
ClassDeclaration(node) {
if (node.id) {
// Classes are hoisted to their containing scope
addDeclaration(node.id.name, 0, true);
}
}
});
// Main traversal
walk.ancestor(ast, {
// Track variable declarations
VariableDeclarator(node) {
const line = getLineNumber(node);
// Special handling for require() - it's synchronous so variables are available immediately
const isRequire = node.init && node.init.type === 'CallExpression' &&
node.init.callee && node.init.callee.name === 'require';
if (node.id.type === 'Identifier') {
addDeclaration(node.id.name, isRequire ? 0 : line);
} else if (node.id.type === 'ObjectPattern') {
// Handle object destructuring
node.id.properties.forEach(prop => {
if (prop.type === 'Property') {
// Handle regular property: { foo }
if (prop.value && prop.value.type === 'Identifier') {
addDeclaration(prop.value.name, line);
}
// Handle property with default: { foo = 'default' }
else if (prop.value && prop.value.type === 'AssignmentPattern' &&
prop.value.left && prop.value.left.type === 'Identifier') {
addDeclaration(prop.value.left.name, isRequire ? 0 : line);
}
}
});
} else if (node.id.type === 'ArrayPattern') {
// Handle array destructuring
node.id.elements.forEach(elem => {
if (elem && elem.type === 'Identifier') {
addDeclaration(elem.name, line);
}
// Handle array destructuring with defaults: [a = 1, b = 2]
else if (elem && elem.type === 'AssignmentPattern' &&
elem.left && elem.left.type === 'Identifier') {
addDeclaration(elem.left.name, line);
}
});
}
},
// Track function parameters and scope (includes arrow functions)
Function(node) {
enterScope();
// Add function name to its own scope (for recursion)
if (node.id) {
addDeclaration(node.id.name, 0);
}
// Add parameters
node.params.forEach(param => {
if (param.type === 'Identifier') {
addDeclaration(param.name, 0);
} else if (param.type === 'ObjectPattern') {
param.properties.forEach(prop => {
if (prop.type === 'Property' && prop.value && prop.value.type === 'Identifier') {
addDeclaration(prop.value.name, 0);
}
});
} else if (param.type === 'ArrayPattern') {
param.elements.forEach(elem => {
if (elem && elem.type === 'Identifier') {
addDeclaration(elem.name, 0);
}
});
} else if (param.type === 'AssignmentPattern') {
// Handle default parameters
if (param.left.type === 'Identifier') {
addDeclaration(param.left.name, 0);
}
} else if (param.type === 'RestElement') {
// Handle rest parameters
if (param.argument.type === 'Identifier') {
addDeclaration(param.argument.name, 0);
}
}
});
},
// Track for loop variables
ForStatement(node) {
if (node.init && node.init.type === 'VariableDeclaration') {
node.init.declarations.forEach(decl => {
if (decl.id.type === 'Identifier') {
addDeclaration(decl.id.name, 0);
}
});
}
},
// Track for...in and for...of loop variables
ForInStatement(node) {
if (node.left.type === 'VariableDeclaration') {
node.left.declarations.forEach(decl => {
if (decl.id.type === 'Identifier') {
addDeclaration(decl.id.name, 0);
}
});
}
},
ForOfStatement(node) {
if (node.left.type === 'VariableDeclaration') {
node.left.declarations.forEach(decl => {
if (decl.id.type === 'Identifier') {
addDeclaration(decl.id.name, 0);
}
});
}
},
'Function:exit'() {
exitScope();
},
// Track block scopes
BlockStatement() {
enterScope();
},
'BlockStatement:exit'() {
exitScope();
},
// Track catch clause parameters
CatchClause(node) {
if (node.param) {
if (node.param.type === 'Identifier') {
addDeclaration(node.param.name, 0);
}
}
},
// Check variable usage
Identifier(node, ancestors) {
const parent = ancestors[ancestors.length - 2];
if (!parent) return;
// Skip if identifier is part of a declaration
if (parent.type === 'VariableDeclarator' && parent.id === node) return;
if (parent.type === 'FunctionDeclaration' && parent.id === node) return;
if (parent.type === 'FunctionExpression' && parent.id === node) return;
if (parent.type === 'MemberExpression' && parent.property === node && !parent.computed) return;
if (parent.type === 'Property' && parent.key === node && !parent.computed) return;
if (parent.type === 'LabeledStatement' && parent.label === node) return;
if (parent.type === 'BreakStatement' && parent.label === node) return;
if (parent.type === 'ContinueStatement' && parent.label === node) return;
// Skip if this is a function parameter
const isParameter = ancestors.some((ancestor, idx) => {
if (ancestor.type === 'FunctionDeclaration' ||
ancestor.type === 'FunctionExpression' ||
ancestor.type === 'ArrowFunctionExpression') {
return ancestor.params.some(param => {
if (param === node) return true;
if (param.type === 'Identifier' && param.name === node.name) return true;
// Check destructured parameters
if (param.type === 'ObjectPattern') {
return param.properties.some(prop => {
if (prop.type === 'Property') {
return (prop.value && prop.value.type === 'Identifier' && prop.value.name === node.name) ||
(prop.key && prop.key.type === 'Identifier' && prop.key.name === node.name && prop.shorthand);
}
return false;
});
}
if (param.type === 'ArrayPattern') {
return param.elements.some(elem =>
elem && elem.type === 'Identifier' && elem.name === node.name
);
}
if (param.type === 'AssignmentPattern') {
// Default parameters
return param.left.type === 'Identifier' && param.left.name === node.name;
}
if (param.type === 'RestElement') {
// Rest parameters
return param.argument.type === 'Identifier' && param.argument.name === node.name;
}
return false;
});
}
return false;
});
if (isParameter) return;
// Check if this is a catch parameter
const isCatchParam = ancestors.some((ancestor, idx) => {
if (ancestor.type === 'CatchClause' && ancestor.param) {
return ancestor.param.type === 'Identifier' && ancestor.param.name === node.name;
}
return false;
});
if (isCatchParam) return;
const name = node.name;
const line = getLineNumber(node);
// Check if it's a global or built-in
if (isGlobalVariable(name) || isBuiltIn(name)) return;
// Skip common Node.js modules
const commonModules = ['fs', 'path', 'http', 'https', 'crypto', 'os', 'url', 'util',
'stream', 'events', 'child_process', 'cluster', 'assert', 'zlib',
'querystring', 'readline', 'vm', 'tty', 'net', 'dns', 'tls'];
if (commonModules.includes(name)) return;
// Check for declaration
const declaration = findDeclaration(name);
if (!declaration) {
bugs.push({
type: 'use_before_init',
severity: 'HIGH',
category: 'Initialization',
line,
column: 0,
summary: `Variable '${name}' used before initialization`,
description: `Variable '${name}' is used but never declared in this scope`,
recommendation: 'Ensure variable is declared and initialized before use',
effort: 1,
impact: 'high',
file: filePath,
variable: name
});
} else if (declaration.line > line) {
// Temporal dead zone
bugs.push({
type: 'use_before_init',
severity: 'HIGH',
category: 'Initialization',
line,
column: 0,
summary: `Variable '${name}' used before initialization (temporal dead zone)`,
description: `Variable '${name}' is accessed before its declaration on line ${declaration.line}`,
recommendation: 'Move variable usage after its declaration',
effort: 1,
impact: 'high',
file: filePath,
variable: name
});
}
}
});
return bugs;
}
module.exports = detectUseBeforeInit;