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