"use strict";
/**
 * Taint Propagation Tracker — Propagates taint through variable assignments.
 *
 * Walks lines forward from discovered sources, tracking how tainted data
 * flows through assignments, destructuring, template literals, etc.
 * Marks variables as sanitised when they pass through known sanitisers.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.propagateTaint = propagateTaint;
const sanitiser_detection_1 = require("./sanitiser-detection");
// ============================================================================
// Scope Tracking
// ============================================================================
/**
 * Track brace depth for approximate scope analysis.
 * Counts { and } while ignoring those inside string literals and comments.
 */
function countBraceChanges(line, inMultiLineComment) {
    let delta = 0;
    let inComment = inMultiLineComment;
    let inString = null;
    let escaped = false;
    for (let i = 0; i < line.length; i++) {
        const ch = line[i];
        const next = i < line.length - 1 ? line[i + 1] : '';
        if (escaped) {
            escaped = false;
            continue;
        }
        if (ch === '\\') {
            escaped = true;
            continue;
        }
        // Handle multi-line comments
        if (inComment) {
            if (ch === '*' && next === '/') {
                inComment = false;
                i++; // skip /
            }
            continue;
        }
        // Handle single-line comments
        if (!inString && ch === '/' && next === '/') {
            break; // rest of line is comment
        }
        // Handle multi-line comment start
        if (!inString && ch === '/' && next === '*') {
            inComment = true;
            i++; // skip *
            continue;
        }
        // Handle string literals
        if (!inString && (ch === '"' || ch === "'" || ch === '`')) {
            inString = ch;
            continue;
        }
        if (inString && ch === inString) {
            inString = null;
            continue;
        }
        // Count braces outside strings and comments
        if (!inString) {
            if (ch === '{')
                delta++;
            if (ch === '}')
                delta--;
        }
    }
    return { delta, inComment };
}
// ============================================================================
// Propagation Detection
// ============================================================================
/** Check if a tainted variable appears on the right side of an expression */
function findTaintedVarInExpression(line, taintedVars) {
    for (const [name, tv] of taintedVars) {
        // Use word boundary to avoid partial matches (e.g., "url" matching "urlParser")
        const regex = new RegExp(`\\b${escapeRegex(name)}\\b`);
        if (regex.test(line)) {
            return { varName: name, variable: tv };
        }
    }
    return null;
}
function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
 * Detect what type of propagation is happening on a line.
 */
function detectPropagationType(line) {
    const trimmed = line.trim();
    // Destructuring: const { a, b } = taintedVar
    if (/(?:const|let|var)\s+\{[^}]+\}\s*=/.test(trimmed)) {
        return 'destructure';
    }
    // Spread: { ...taintedVar }
    if (/\.\.\.\w+/.test(trimmed)) {
        return 'spread';
    }
    // Template literal: `${taintedVar}`
    if (/`[^`]*\$\{[^}]+\}[^`]*`/.test(trimmed)) {
        return 'template_literal';
    }
    // String concatenation: "..." + taintedVar
    if (/['"][^'"]*['"]\s*\+/.test(trimmed) || /\+\s*['"][^'"]*['"]/.test(trimmed)) {
        return 'string_concat';
    }
    // Ternary/nullish: taintedVar ?? default, taintedVar || default
    if (/\?\?|\|\|/.test(trimmed) && /(?:const|let|var)\s+\w+\s*=/.test(trimmed)) {
        return 'ternary';
    }
    // Array push: arr.push(taintedVar)
    if (/\.push\s*\(/.test(trimmed)) {
        return 'array_push';
    }
    // Object spread: { ...taintedVar, key: value }
    if (/\{[^}]*\.\.\.\w+/.test(trimmed)) {
        return 'object_spread';
    }
    // Method chain: taintedVar.trim().toLowerCase() — result still tainted
    if (/\.\w+\s*\(/.test(trimmed)) {
        return 'method_chain';
    }
    // Direct assignment: const x = taintedVar
    if (/(?:const|let|var)\s+\w+\s*=/.test(trimmed) || /\w+\s*=(?!=)/.test(trimmed)) {
        return 'direct_assignment';
    }
    return null;
}
/**
 * Extract the newly assigned variable name from a line.
 */
function extractAssignedVariable(line) {
    const trimmed = line.trim();
    // Destructuring: const { a, b } = ...
    const destructMatch = trimmed.match(/(?:const|let|var)\s+\{\s*([^}]+)\}\s*=/);
    if (destructMatch) {
        return destructMatch[1].split(',').map(n => {
            const parts = n.trim().split(':');
            return (parts.length > 1 ? parts[1] : parts[0]).trim();
        }).filter(n => n.length > 0 && n !== '...');
    }
    // Array destructuring: const [a, b] = ...
    const arrayMatch = trimmed.match(/(?:const|let|var)\s+\[\s*([^\]]+)\]\s*=/);
    if (arrayMatch) {
        return arrayMatch[1].split(',').map(n => n.trim()).filter(n => n.length > 0 && n !== '...');
    }
    // Direct assignment: const x = ... or let x = ...
    const directMatch = trimmed.match(/(?:const|let|var)\s+(\w+)\s*=/);
    if (directMatch) {
        return [directMatch[1]];
    }
    // Re-assignment: x = ... (no const/let/var)
    const reassignMatch = trimmed.match(/^(\w+)\s*=(?!=)/);
    if (reassignMatch) {
        return [reassignMatch[1]];
    }
    return [];
}
/**
 * Determine variable scope type from declaration keyword.
 */
function getDeclarationType(line) {
    const trimmed = line.trim();
    if (/^(?:const|let)\b/.test(trimmed))
        return 'block';
    if (/^var\b/.test(trimmed))
        return 'function';
    return 'reassign';
}
// ============================================================================
// Main Propagation Function
// ============================================================================
/**
 * Propagate taint through variable assignments in a file.
 *
 * Algorithm:
 * 1. Initialize tainted vars from sources
 * 2. Walk lines forward, tracking brace depth for scope
 * 3. For each line, check if any tainted var appears on the right side
 * 4. If sanitiser wraps the expression, mark sanitised
 * 5. When exiting a scope, remove block-scoped (const/let) vars at that depth
 */
function propagateTaint(content, _filePath, sources, sanitisers) {
    const lines = content.split('\n');
    const taintedVars = new Map();
    const allTaintedVars = [];
    // Track scope for variable lifetime
    let braceDepth = 0;
    let inMultiLineComment = false;
    const scopeVars = new Map(); // variable name -> declared scope depth
    // Initialize tainted vars from sources
    for (const source of sources) {
        const tv = {
            name: source.variable,
            taintedAt: source.line,
            source,
            propagation: 'direct_assignment',
            sanitised: false,
            scopeDepth: 0,
        };
        taintedVars.set(source.variable, tv);
        allTaintedVars.push(tv);
    }
    // Walk lines forward
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const lineNumber = i + 1;
        const trimmed = line.trim();
        // Skip empty lines and comments
        if (trimmed === '' || trimmed.startsWith('//')) {
            continue;
        }
        // Update brace depth
        const braceResult = countBraceChanges(line, inMultiLineComment);
        inMultiLineComment = braceResult.inComment;
        // Handle scope exit: remove block-scoped vars declared at the exiting depth
        if (braceResult.delta < 0) {
            const newDepth = braceDepth + braceResult.delta;
            for (const [varName, declDepth] of scopeVars) {
                if (declDepth > newDepth) {
                    taintedVars.delete(varName);
                    scopeVars.delete(varName);
                }
            }
        }
        braceDepth = Math.max(0, braceDepth + braceResult.delta);
        // Skip lines that are only closing braces
        if (/^[}\])\s;]*$/.test(trimmed))
            continue;
        // Check if any tainted variable appears in this line (on the right side of an assignment)
        const found = findTaintedVarInExpression(line, taintedVars);
        if (!found)
            continue;
        // Check if this line is an assignment that creates a new tainted variable
        const assignedVars = extractAssignedVariable(line);
        if (assignedVars.length === 0)
            continue;
        // Don't re-taint the same variable (e.g., `userInput = userInput.trim()`)
        const newVars = assignedVars.filter(v => v !== found.varName);
        if (newVars.length === 0) {
            // Still might be a method chain on the same variable — update it
            // Check if sanitiser is applied
            const sanitResult = (0, sanitiser_detection_1.isSanitiserCall)(line, sanitisers);
            if (sanitResult.sanitised && taintedVars.has(found.varName)) {
                const existing = taintedVars.get(found.varName);
                existing.sanitised = true;
                existing.sanitisedAt = lineNumber;
                existing.sanitisationMethod = sanitResult.method;
            }
            continue;
        }
        // Detect propagation type
        const propagationType = detectPropagationType(line) ?? 'direct_assignment';
        const declType = getDeclarationType(line);
        // Check if sanitised at this assignment
        const sanitResult = (0, sanitiser_detection_1.isSanitiserCall)(line, sanitisers);
        for (const varName of newVars) {
            const tv = {
                name: varName,
                taintedAt: lineNumber,
                source: found.variable.source,
                propagation: propagationType,
                sanitised: sanitResult.sanitised,
                sanitisedAt: sanitResult.sanitised ? lineNumber : undefined,
                sanitisationMethod: sanitResult.method,
                scopeDepth: braceDepth,
            };
            taintedVars.set(varName, tv);
            allTaintedVars.push(tv);
            // Track scope for block-scoped vars
            if (declType === 'block') {
                scopeVars.set(varName, braceDepth);
            }
        }
    }
    return allTaintedVars;
}
//# sourceMappingURL=taint-tracker.js.map