/**
 * AST Query Engine
 *
 * Pattern-based AST matching using ts-morph for TypeScript/JavaScript.
 * Supports parameterized patterns with capture groups.
 *
 * @module scanners/detection/engines/ast-query
 */
import { Project, Node } from "ts-morph";
import { readFile } from "fs/promises";
import { glob } from "glob";
const project = new Project({
    useInMemoryFileSystem: false,
    skipFileDependencyResolution: true,
});
function parsePattern(pattern) {
    const parts = [];
    let i = 0;
    while (i < pattern.length) {
        if (pattern[i] === "$" && i + 1 < pattern.length) {
            const start = i + 1;
            let end = start;
            while (end < pattern.length && /[a-zA-Z0-9_]/.test(pattern[end])) {
                end++;
            }
            if (end > start) {
                parts.push({ type: "capture", value: pattern.slice(start, end), name: pattern.slice(start, end) });
                i = end;
                continue;
            }
        }
        if (pattern.slice(i, i + 3) === "...") {
            parts.push({ type: "wildcard", value: "..." });
            i += 3;
            continue;
        }
        let literalEnd = i;
        while (literalEnd < pattern.length && pattern[literalEnd] !== "$" && pattern.slice(literalEnd, literalEnd + 3) !== "...") {
            literalEnd++;
        }
        if (literalEnd > i) {
            parts.push({ type: "literal", value: pattern.slice(i, literalEnd) });
            i = literalEnd;
        }
    }
    return parts;
}
function matchPattern(text, parts) {
    const captures = {};
    let textIdx = 0;
    for (let i = 0; i < parts.length; i++) {
        const part = parts[i];
        if (part.type === "literal") {
            const literal = part.value.trim();
            const remaining = text.slice(textIdx).trim();
            if (!remaining.startsWith(literal)) {
                return { matched: false, captures: {} };
            }
            textIdx = text.indexOf(literal, textIdx) + literal.length;
        }
        else if (part.type === "capture") {
            const nextPart = parts[i + 1];
            let endIdx;
            if (!nextPart) {
                endIdx = text.length;
            }
            else if (nextPart.type === "literal") {
                const nextLiteral = nextPart.value.trim();
                endIdx = text.indexOf(nextLiteral, textIdx);
                if (endIdx === -1) {
                    return { matched: false, captures: {} };
                }
            }
            else {
                endIdx = text.length;
            }
            const captured = text.slice(textIdx, endIdx).trim();
            if (part.name) {
                captures[part.name] = captured;
            }
            textIdx = endIdx;
        }
        else if (part.type === "wildcard") {
            const nextPart = parts[i + 1];
            if (!nextPart) {
                textIdx = text.length;
            }
            else if (nextPart.type === "literal") {
                const nextLiteral = nextPart.value.trim();
                const idx = text.indexOf(nextLiteral, textIdx);
                if (idx === -1) {
                    return { matched: false, captures: {} };
                }
                textIdx = idx;
            }
        }
    }
    return { matched: true, captures };
}
function findCallExpressions(sourceFile, pattern) {
    const matches = [];
    const parts = parsePattern(pattern);
    sourceFile.forEachDescendant((node) => {
        if (Node.isCallExpression(node)) {
            const text = node.getText();
            const result = matchPattern(text, parts);
            if (result.matched) {
                matches.push({
                    node,
                    file: sourceFile.getFilePath(),
                    line: node.getStartLineNumber(),
                    column: node.getStartLinePos() - node.getStartLinePos(true) + 1,
                    endLine: node.getEndLineNumber(),
                    endColumn: node.getEnd() - node.getStartLinePos(true) + 1,
                    text,
                    captures: result.captures,
                });
            }
        }
    });
    return matches;
}
function findPropertyAccess(sourceFile, pattern) {
    const matches = [];
    const parts = parsePattern(pattern);
    sourceFile.forEachDescendant((node) => {
        if (Node.isPropertyAccessExpression(node) || Node.isElementAccessExpression(node)) {
            const text = node.getText();
            const result = matchPattern(text, parts);
            if (result.matched) {
                matches.push({
                    node,
                    file: sourceFile.getFilePath(),
                    line: node.getStartLineNumber(),
                    column: node.getStartLinePos() - node.getStartLinePos(true) + 1,
                    endLine: node.getEndLineNumber(),
                    endColumn: node.getEnd() - node.getStartLinePos(true) + 1,
                    text,
                    captures: result.captures,
                });
            }
        }
    });
    return matches;
}
function findAssignments(sourceFile, pattern) {
    const matches = [];
    const parts = parsePattern(pattern);
    sourceFile.forEachDescendant((node) => {
        if (Node.isBinaryExpression(node) && node.getOperatorToken().getText() === "=") {
            const text = node.getText();
            const result = matchPattern(text, parts);
            if (result.matched) {
                matches.push({
                    node,
                    file: sourceFile.getFilePath(),
                    line: node.getStartLineNumber(),
                    column: node.getStartLinePos() - node.getStartLinePos(true) + 1,
                    endLine: node.getEndLineNumber(),
                    endColumn: node.getEnd() - node.getStartLinePos(true) + 1,
                    text,
                    captures: result.captures,
                });
            }
        }
    });
    return matches;
}
export async function queryAST(filePath, config) {
    try {
        const content = await readFile(filePath, "utf-8");
        const sourceFile = project.createSourceFile(`query_${Date.now()}_${Math.random().toString(36).slice(2)}.ts`, content, { overwrite: true });
        let matches = [];
        const pattern = config.pattern;
        if (pattern.includes("(") && pattern.includes(")")) {
            matches = findCallExpressions(sourceFile, pattern);
        }
        else if (pattern.includes(".") || pattern.includes("[")) {
            matches = findPropertyAccess(sourceFile, pattern);
        }
        else if (pattern.includes("=")) {
            matches = findAssignments(sourceFile, pattern);
        }
        else {
            matches = [
                ...findCallExpressions(sourceFile, pattern),
                ...findPropertyAccess(sourceFile, pattern),
            ];
        }
        sourceFile.delete();
        return matches.map((m) => ({ ...m, file: filePath }));
    }
    catch {
        return [];
    }
}
export async function runASTQueryEngine(projectPath, rules, files) {
    const matches = [];
    const targetFiles = files || (await glob("**/*.{ts,tsx,js,jsx}", {
        cwd: projectPath,
        ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**"],
        absolute: true,
    }));
    for (const rule of rules) {
        if (!rule.engines.astQuery)
            continue;
        const config = rule.engines.astQuery;
        for (const file of targetFiles) {
            const astMatches = await queryAST(file, config);
            for (const match of astMatches) {
                matches.push({
                    ruleId: rule.id,
                    file: match.file,
                    line: match.line,
                    column: match.column,
                    endLine: match.endLine,
                    endColumn: match.endColumn,
                    message: rule.description,
                    severity: rule.severity,
                    confidence: rule.confidence,
                    category: rule.category,
                    evidence: match.text,
                    cweIds: rule.cweIds,
                    owaspRefs: rule.owaspRefs,
                    autofixPatternId: rule.autofixPatternId,
                });
            }
        }
    }
    return matches;
}
//# sourceMappingURL=ast-query.js.map