import { Parser } from 'web-tree-sitter';
import { WASMLoader } from './languages/wasm-loader.js';
import fs from 'fs';
export class TaintAnalyzer {
    constructor(context, matcher) {
        this.context = context;
        this.matcher = matcher;
        this.visitedParameters = new Set();
        this.MAX_RECURSION_DEPTH = 5;
        this.treeCache = new Map();
    }
    cleanup() {
        for (const tree of this.treeCache.values()) {
            tree.delete();
        }
        this.treeCache.clear();
        this.visitedParameters.clear();
    }
    /**
     * Traces a node back to its sources and returns all unique labels found.
     */
    async getTaintLabels(node, lang, rule, parser, depth = 0) {
        if (depth > this.MAX_RECURSION_DEPTH)
            return new Set();
        const labels = new Set();
        // 1. Handle safe literals and constants early
        if (this.isLiteral(node) || this.isSafeConstant(node)) {
            return labels;
        }
        // 2. Check if this node itself is a sanitizer in this rule
        if (await this.isMatchingAnyPattern(node, rule['pattern-sanitizers'] || [], parser, lang)) {
            // If sanitized, we yield no labels for this path
            return labels;
        }
        // 3. Check if this node itself is a source in this rule
        const sourceLabels = await this.getMatchingSourceLabels(node, rule['pattern-sources'] || [], parser, lang);
        for (const label of sourceLabels) {
            labels.add(label);
        }
        // 4. Trace the definition further back
        const varName = node.text;
        const definition = this.findDefinition(node, varName, lang);
        if (!definition) {
            // If it's a parameter, we can trace it to call sites
            const parentFunc = this.findParentFunction(node);
            if (parentFunc) {
                const callSiteLabels = await this.getParameterTaintLabels(parentFunc, varName, lang, rule, parser, depth + 1);
                for (const label of callSiteLabels)
                    labels.add(label);
            }
        }
        else {
            // Recursive trace on the definition
            const defLabels = await this.getTaintLabels(definition, lang, rule, parser, depth);
            for (const label of defLabels)
                labels.add(label);
        }
        return labels;
    }
    async isMatchingAnyPattern(node, clauses, parser, lang) {
        for (const clause of clauses) {
            const patterns = this.extractPatternsFromClause(clause);
            for (const pattern of patterns) {
                if (this.matchesPattern(node, pattern, parser, lang))
                    return true;
            }
        }
        return false;
    }
    async getMatchingSourceLabels(node, sources, parser, lang) {
        const matchedLabels = [];
        for (const source of sources) {
            const patterns = this.extractPatternsFromClause(source);
            for (const pattern of patterns) {
                if (this.matchesPattern(node, pattern, parser, lang)) {
                    if (source.label)
                        matchedLabels.push(source.label);
                    // If no label, it's a generic source (though usually they have labels in taint mode)
                }
            }
        }
        return matchedLabels;
    }
    extractPatternsFromClause(clause) {
        const patterns = [];
        if (clause.pattern)
            patterns.push(clause.pattern);
        if (clause['pattern-either']) {
            for (const p of clause['pattern-either']) {
                if (typeof p === 'string')
                    patterns.push(p);
                else
                    patterns.push(...this.extractPatternsFromClause(p));
            }
        }
        if (clause.patterns) {
            for (const p of clause.patterns) {
                if (typeof p === 'string')
                    patterns.push(p);
                else
                    patterns.push(...this.extractPatternsFromClause(p));
            }
        }
        return patterns;
    }
    matchesPattern(node, patternStr, parser, lang) {
        let p = patternStr;
        if (lang === 'php' && !p.includes('<?php')) {
            p = `<?php ${p}`;
            if (!p.endsWith(';') && !p.endsWith('}'))
                p += ';';
        }
        try {
            const tree = parser.parse(p);
            if (!tree)
                return false;
            // Get the actual node to match (skip program/expression_statement wrapper)
            let patternNode = tree.rootNode;
            while (patternNode.childCount === 1 && (patternNode.type === 'program' || patternNode.type === 'expression_statement')) {
                patternNode = patternNode.firstChild;
            }
            const results = this.matcher.match(patternNode, node);
            return results.length > 0;
        }
        catch (e) {
            return false;
        }
    }
    findDefinition(node, varName, lang) {
        let current = node;
        while (current && !this.isScopeRoot(current)) {
            let sibling = current.previousNamedSibling;
            while (sibling) {
                const def = this.extractDefinitionFromStatement(sibling, varName, lang);
                if (def)
                    return def;
                sibling = sibling.previousNamedSibling;
            }
            current = current.parent;
        }
        return null;
    }
    isScopeRoot(node) {
        const types = ['statement_block', 'compound_statement', 'program', 'function_definition', 'method_definition'];
        return types.includes(node.type);
    }
    isFunction(node) {
        const types = ['function_definition', 'method_definition', 'arrow_function', 'function_declaration'];
        return types.includes(node.type);
    }
    extractDefinitionFromStatement(node, varName, lang) {
        if (node.type === 'lexical_declaration' || node.type === 'variable_declaration') {
            const declarator = node.descendantsOfType('variable_declarator').find(d => d.childForFieldName('name')?.text === varName);
            return declarator?.childForFieldName('value') || null;
        }
        if (node.type === 'assignment') {
            const left = node.childForFieldName('left');
            if (left?.text === varName)
                return node.childForFieldName('right');
        }
        if (node.type === 'expression_statement') {
            const assign = node.firstChild;
            if (assign?.type === 'assignment_expression') {
                const left = assign.childForFieldName('left');
                if (left?.text === varName || left?.text === `$${varName}`)
                    return assign.childForFieldName('right');
            }
        }
        if (node.type === 'short_var_declaration' || node.type === 'assignment_statement') {
            const left = node.childForFieldName('left');
            if (left?.text.includes(varName))
                return node.childForFieldName('right');
        }
        if (node.type === 'assignment_expression') {
            const left = node.childForFieldName('left');
            if (left?.text === varName)
                return node.childForFieldName('right');
        }
        return null;
    }
    findParentFunction(node) {
        let current = node;
        while (current) {
            if (this.isFunction(current))
                return current;
            current = current.parent;
        }
        return null;
    }
    async getParameterTaintLabels(funcNode, varName, lang, rule, parser, depth) {
        const labels = new Set();
        const funcName = funcNode.childForFieldName('name')?.text ||
            funcNode.parent?.childForFieldName('name')?.text;
        if (!funcName)
            return labels;
        const paramId = `${funcName}:${varName}`;
        if (this.visitedParameters.has(paramId))
            return labels;
        this.visitedParameters.add(paramId);
        const calls = this.context.callSites[funcName];
        if (!calls || calls.length === 0)
            return labels;
        const params = funcNode.childForFieldName('parameters')?.descendantsOfType('identifier').map(i => i.text) || [];
        const paramIndex = params.indexOf(varName);
        if (paramIndex === -1)
            return labels;
        for (const call of calls) {
            const argPos = call.argPositions[paramIndex];
            if (!argPos)
                continue;
            try {
                const callerRoot = await this.getTreeRoot(call.filePath, lang);
                if (!callerRoot)
                    continue;
                const argNode = callerRoot.descendantForPosition({ row: argPos.row, column: argPos.column });
                if (!argNode)
                    continue;
                const callLabels = await this.getTaintLabels(argNode, lang, rule, parser, depth);
                for (const l of callLabels)
                    labels.add(l);
            }
            catch (e) { }
        }
        return labels;
    }
    async getTreeRoot(filePath, langName) {
        if (this.treeCache.has(filePath))
            return this.treeCache.get(filePath).rootNode;
        try {
            const content = fs.readFileSync(filePath, 'utf-8');
            const lang = await WASMLoader.getLanguage(langName);
            const parser = new Parser();
            parser.setLanguage(lang);
            const tree = parser.parse(content);
            if (tree) {
                this.treeCache.set(filePath, tree);
                return tree.rootNode;
            }
        }
        catch (e) { }
        return null;
    }
    isLiteral(node) {
        const types = ['string', 'number', 'boolean', 'string_literal', 'number_literal'];
        if (types.some(t => node.type.includes(t)))
            return true;
        const text = node.text;
        if (text === 'null' || text === 'undefined' || text === 'true' || text === 'false')
            return true;
        return false;
    }
    isSafeConstant(node) {
        const text = node.text;
        const safeNames = ['__dirname', '__filename', 'process', 'module', 'exports', 'require'];
        return safeNames.some(s => text.startsWith(s));
    }
}
//# sourceMappingURL=taint-analyzer.js.map