UNPKG

agentsqripts

Version:

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

381 lines (338 loc) 12.9 kB
/** * @file Data flow analyzer for comprehensive variable state tracking and bug detection * @description Single responsibility: Track variable states throughout execution paths to detect flow-related bugs * * This analyzer performs sophisticated data flow analysis to track variable states, assignments, * and usage patterns throughout different execution paths. It identifies bugs related to variable * initialization, state consistency, and data flow violations that could lead to runtime errors * or unexpected behavior in production applications. * * Design rationale: * - State-based tracking maintains variable lifecycle information throughout code execution paths * - Control flow analysis handles conditional branches and loop constructs for comprehensive coverage * - Assignment pattern tracking identifies inconsistent variable state changes across execution paths * - Context-sensitive analysis provides accurate bug detection in complex control flow scenarios * - Path-sensitive analysis reduces false positives by considering actual execution possibilities * * Data flow analysis scope: * - Variable initialization tracking across all possible execution paths * - Assignment consistency validation in conditional branches and loops * - State mutation detection for complex object and array modifications * - Control flow impact analysis for variable accessibility and lifetime management * - Dead code detection through unreachable variable assignments and usage patterns */ const walk = require('acorn-walk'); const { getLineNumber } = require('../utils/astParser'); /** * Variable state tracker for data flow analysis */ class DataFlowTracker { constructor() { this.variables = new Map(); // name -> { type, value, tainted, defined } this.scopes = [new Map()]; // Stack of scopes } enterScope() { this.scopes.push(new Map()); } exitScope() { this.scopes.pop(); } setVariable(name, info) { const currentScope = this.scopes[this.scopes.length - 1]; currentScope.set(name, info); } getVariable(name) { // Search from current scope upwards for (let i = this.scopes.length - 1; i >= 0; i--) { if (this.scopes[i].has(name)) { return this.scopes[i].get(name); } } return null; } isArrayType(name) { const info = this.getVariable(name); return info && info.type === 'array'; } isTainted(name) { const info = this.getVariable(name); return info && info.tainted; } } /** * Perform data flow analysis * @param {Object} ast - AST tree * @param {string} filePath - File path * @returns {Array} Detected bugs */ function analyzeDataFlow(ast, filePath) { const bugs = []; const tracker = new DataFlowTracker(); bugs.push(...detectArrayBoundsIssues(ast, filePath, tracker)); bugs.push(...detectTaintedData(ast, filePath, tracker)); bugs.push(...detectUnhandledEdgeCases(ast, filePath, tracker)); return bugs; } /** * Detect array bounds issues * @param {Object} ast - AST tree * @param {string} filePath - File path * @param {DataFlowTracker} tracker - Data flow tracker * @returns {Array} Detected bugs */ function detectArrayBoundsIssues(ast, filePath, tracker) { const bugs = []; walk.simple(ast, { // Track array declarations VariableDeclarator(node) { if (node.init && node.init.type === 'ArrayExpression') { tracker.setVariable(node.id.name, { type: 'array', length: node.init.elements.length, defined: true }); } }, // Check array access MemberExpression(node) { if (node.computed && node.object.type === 'Identifier') { const arrayName = node.object.name; if (tracker.isArrayType(arrayName)) { // Check for negative index if (node.property.type === 'UnaryExpression' && node.property.operator === '-') { bugs.push({ type: 'negative_array_index', severity: 'HIGH', category: 'Data Flow', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: `Negative array index on '${arrayName}'`, recommendation: 'Check array bounds before accessing', effort: 1, impact: 'high', file: filePath }); } // Check for hardcoded large index if (node.property.type === 'Literal' && typeof node.property.value === 'number' && node.property.value > 100) { bugs.push({ type: 'large_array_index', severity: 'MEDIUM', category: 'Data Flow', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: `Large hardcoded array index ${node.property.value} on '${arrayName}'`, recommendation: 'Verify array length before accessing large indices', effort: 1, impact: 'medium', file: filePath }); } } } } }); return bugs; } /** * Detect tainted data usage * @param {Object} ast - AST tree * @param {string} filePath - File path * @param {DataFlowTracker} tracker - Data flow tracker * @returns {Array} Detected bugs */ function detectTaintedData(ast, filePath, tracker) { const bugs = []; walk.simple(ast, { // Track user input sources CallExpression(node) { const callee = node.callee; // Check for user input functions if (callee.type === 'MemberExpression') { const obj = callee.object.name; const method = callee.property.name; // Mark as tainted if from user input if ((obj === 'req' && ['body', 'query', 'params'].includes(method)) || (obj === 'document' && ['getElementById', 'querySelector'].includes(method))) { // Find assignment let parent = node; // This is simplified - would need proper parent tracking tracker.setVariable('userInput', { tainted: true, defined: true }); } } // Check for dangerous operations with tainted data if (callee.name === 'eval' || (callee.type === 'MemberExpression' && callee.property.name === 'innerHTML')) { bugs.push({ type: 'tainted_data_usage', severity: 'HIGH', category: 'Security', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: 'Potentially unsafe operation with user input', recommendation: 'Sanitize user input before using in dangerous operations', effort: 2, impact: 'high', file: filePath }); } } }); return bugs; } /** * Detect unhandled edge cases * @param {Object} ast - AST tree * @param {string} filePath - File path * @param {DataFlowTracker} tracker - Data flow tracker * @returns {Array} Detected bugs */ function detectUnhandledEdgeCases(ast, filePath, tracker) { const bugs = []; const zeroCheckedVars = new Set(); // Track variables that have been checked for zero const safeScopes = new Map(); // Track scopes where variables are guaranteed non-zero // First pass: find zero checks and track safe scopes walk.ancestor(ast, { IfStatement(node, ancestors) { // Check if this is a zero check (e.g., if (total === 0) or if (total == 0)) if (node.test.type === 'BinaryExpression' && (node.test.operator === '===' || node.test.operator === '==') && node.test.left.type === 'Identifier' && node.test.right.type === 'Literal' && node.test.right.value === 0) { const varName = node.test.left.name; // Check if the consequent has a return statement if (hasEarlyReturn(node.consequent)) { // This variable is protected by an early return zeroCheckedVars.add(varName); } // Track that the else block is safe for this variable if (node.alternate) { if (!safeScopes.has(node.alternate)) { safeScopes.set(node.alternate, new Set()); } safeScopes.get(node.alternate).add(varName); } } // Also check for inverted condition (e.g., if (0 === total)) if (node.test.type === 'BinaryExpression' && (node.test.operator === '===' || node.test.operator === '==') && node.test.right.type === 'Identifier' && node.test.left.type === 'Literal' && node.test.left.value === 0) { const varName = node.test.right.name; if (hasEarlyReturn(node.consequent)) { zeroCheckedVars.add(varName); } // Track that the else block is safe for this variable if (node.alternate) { if (!safeScopes.has(node.alternate)) { safeScopes.set(node.alternate, new Set()); } safeScopes.get(node.alternate).add(varName); } } // Also handle not-equal zero checks (e.g., if (total !== 0) or if (total != 0)) if (node.test.type === 'BinaryExpression' && (node.test.operator === '!==' || node.test.operator === '!=') && node.test.left.type === 'Identifier' && node.test.right.type === 'Literal' && node.test.right.value === 0) { const varName = node.test.left.name; // Track that the consequent (then block) is safe for this variable if (node.consequent) { if (!safeScopes.has(node.consequent)) { safeScopes.set(node.consequent, new Set()); } safeScopes.get(node.consequent).add(varName); } } } }); // Helper function to check if a block contains an early return function hasEarlyReturn(node) { let hasReturn = false; walk.simple(node, { ReturnStatement() { hasReturn = true; }, ThrowStatement() { hasReturn = true; } }); return hasReturn; } // Helper function to check if a node is within a safe scope for a variable function isInSafeScope(node, varName, ancestors) { // Check each ancestor to see if it's a safe scope for this variable for (const ancestor of ancestors) { if (safeScopes.has(ancestor) && safeScopes.get(ancestor).has(varName)) { return true; } } return false; } // Second pass: check for divisions walk.ancestor(ast, { // Check for division by zero potential BinaryExpression(node, ancestors) { if (node.operator === '/' || node.operator === '%') { if (node.right.type === 'Literal' && node.right.value === 0) { bugs.push({ type: 'division_by_zero', severity: 'HIGH', category: 'Data Flow', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: 'Division by zero', recommendation: 'Check divisor before performing division', effort: 1, impact: 'high', file: filePath }); } else if (node.right.type === 'Identifier') { // Only warn if the variable hasn't been checked for zero AND we're not in a safe scope if (!zeroCheckedVars.has(node.right.name) && !isInSafeScope(node, node.right.name, ancestors)) { bugs.push({ type: 'potential_division_by_zero', severity: 'MEDIUM', category: 'Data Flow', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: `Potential division by zero with variable '${node.right.name}'`, recommendation: 'Add zero check before division', effort: 1, impact: 'medium', file: filePath }); } } } }, // Check for parseInt without radix (more accurate with AST) CallExpression(node) { if (node.callee.name === 'parseInt' && node.arguments.length === 1) { bugs.push({ type: 'missing_radix', severity: 'MEDIUM', category: 'Type Safety', line: getLineNumber(node), column: node.loc ? node.loc.start.column : 0, description: 'parseInt() called without radix parameter', recommendation: 'Always specify radix (usually 10) as second parameter', effort: 1, impact: 'medium', file: filePath }); } } }); return bugs; } module.exports = { analyzeDataFlow, DataFlowTracker };