UNPKG

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