"use strict";
// ---------------------------------------------------------------------------
// Data Flow Analysis for Inline Workflows
//
// Given a workflow function (the callback inside Steps.createFunction()),
// finds all free variables — identifiers that reference declarations outside
// the function — and classifies each as a service binding, compile-time
// constant, or runtime expression.
// ---------------------------------------------------------------------------
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeFreeVariables = analyzeFreeVariables;
const typescript_1 = __importDefault(require("typescript"));
const cdkDetection_js_1 = require("../compiler/analysis/cdkDetection.js");
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Analyze a workflow function to find and classify all free variables.
 *
 * A "free variable" is any identifier used inside the function that is
 * declared outside it — excluding the function's own parameters.
 */
function analyzeFreeVariables(checker, workflowFn, serviceRegistry) {
    const body = workflowFn.body;
    if (!body)
        return [];
    // Collect all parameter symbols so we can skip them
    const paramSymbols = new Set();
    for (const param of workflowFn.parameters) {
        const sym = checker.getSymbolAtLocation(param.name);
        if (sym)
            paramSymbols.add(sym);
    }
    // Collect all locally-declared symbols within the function body
    const localSymbols = new Set();
    collectLocalDeclarations(checker, body, localSymbols);
    // Walk the function body and collect all referenced external symbols
    const externalRefs = new Map();
    collectExternalReferences(checker, body, paramSymbols, localSymbols, externalRefs);
    // Classify each external reference
    const results = [];
    for (const [symbol, _identifier] of externalRefs) {
        const classified = classifyFreeVariable(checker, symbol, serviceRegistry);
        results.push(classified);
    }
    return results;
}
// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------
/**
 * Collect all symbols declared within a node (variable declarations,
 * function declarations, class declarations, etc.).
 */
function collectLocalDeclarations(checker, node, locals) {
    typescript_1.default.forEachChild(node, child => {
        // Variable declarations
        if (typescript_1.default.isVariableDeclaration(child) && typescript_1.default.isIdentifier(child.name)) {
            const sym = checker.getSymbolAtLocation(child.name);
            if (sym)
                locals.add(sym);
        }
        // Binding patterns (destructuring)
        if (typescript_1.default.isVariableDeclaration(child) && typescript_1.default.isObjectBindingPattern(child.name)) {
            for (const element of child.name.elements) {
                if (typescript_1.default.isIdentifier(element.name)) {
                    const sym = checker.getSymbolAtLocation(element.name);
                    if (sym)
                        locals.add(sym);
                }
            }
        }
        if (typescript_1.default.isVariableDeclaration(child) && typescript_1.default.isArrayBindingPattern(child.name)) {
            for (const element of child.name.elements) {
                if (typescript_1.default.isBindingElement(element) && typescript_1.default.isIdentifier(element.name)) {
                    const sym = checker.getSymbolAtLocation(element.name);
                    if (sym)
                        locals.add(sym);
                }
            }
        }
        // Function and class declarations
        if (typescript_1.default.isFunctionDeclaration(child) && child.name) {
            const sym = checker.getSymbolAtLocation(child.name);
            if (sym)
                locals.add(sym);
        }
        if (typescript_1.default.isClassDeclaration(child) && child.name) {
            const sym = checker.getSymbolAtLocation(child.name);
            if (sym)
                locals.add(sym);
        }
        // Don't recurse into nested function bodies — their locals are their own scope
        if (typescript_1.default.isFunctionDeclaration(child) || typescript_1.default.isFunctionExpression(child) ||
            typescript_1.default.isArrowFunction(child) || typescript_1.default.isMethodDeclaration(child)) {
            // Only collect parameter names, not body locals
            for (const param of child.parameters) {
                if (typescript_1.default.isIdentifier(param.name)) {
                    const sym = checker.getSymbolAtLocation(param.name);
                    if (sym)
                        locals.add(sym);
                }
            }
            return;
        }
        collectLocalDeclarations(checker, child, locals);
    });
}
/**
 * Walk a node and collect all identifiers that reference symbols
 * not in the parameter set or local declaration set.
 */
function collectExternalReferences(checker, node, paramSymbols, localSymbols, externals) {
    typescript_1.default.forEachChild(node, child => {
        if (typescript_1.default.isIdentifier(child)) {
            // Skip identifiers that are declaration names (not references)
            if (isDeclarationName(child))
                return;
            // Skip identifiers on the right side of property access (a.b — "b" is not a reference)
            if (typescript_1.default.isPropertyAccessExpression(child.parent) && child.parent.name === child)
                return;
            // Skip type-only positions (type annotations, type arguments)
            if (isInTypePosition(child))
                return;
            const sym = checker.getSymbolAtLocation(child);
            if (!sym)
                return;
            // Resolve aliases (imports)
            const resolved = sym.flags & typescript_1.default.SymbolFlags.Alias
                ? checker.getAliasedSymbol(sym)
                : sym;
            if (paramSymbols.has(resolved) || localSymbols.has(resolved))
                return;
            // Skip well-known globals (Error, Promise, JSON, console, etc.)
            if (isWellKnownGlobal(child.text))
                return;
            if (!externals.has(resolved)) {
                externals.set(resolved, child);
            }
        }
        collectExternalReferences(checker, child, paramSymbols, localSymbols, externals);
    });
}
/**
 * Check if an identifier is the name in a declaration (not a reference).
 */
function isDeclarationName(node) {
    const parent = node.parent;
    if (!parent)
        return false;
    if (typescript_1.default.isVariableDeclaration(parent) && parent.name === node)
        return true;
    if (typescript_1.default.isFunctionDeclaration(parent) && parent.name === node)
        return true;
    if (typescript_1.default.isClassDeclaration(parent) && parent.name === node)
        return true;
    if (typescript_1.default.isParameter(parent) && parent.name === node)
        return true;
    if (typescript_1.default.isPropertyAssignment(parent) && parent.name === node)
        return true;
    if (typescript_1.default.isBindingElement(parent) && parent.name === node)
        return true;
    if (typescript_1.default.isImportSpecifier(parent) && parent.name === node)
        return true;
    if (typescript_1.default.isMethodDeclaration(parent) && parent.name === node)
        return true;
    return false;
}
/**
 * Check if a node is in a type-only position (type annotation, type argument, etc.).
 */
function isInTypePosition(node) {
    let current = node.parent;
    while (current) {
        if (typescript_1.default.isTypeNode(current))
            return true;
        if (typescript_1.default.isTypeReferenceNode(current))
            return true;
        // Heritage clauses (extends/implements)
        if (typescript_1.default.isHeritageClause(current))
            return true;
        current = current.parent;
    }
    return false;
}
/** Names that should not be treated as free variables. */
const WELL_KNOWN_GLOBALS = new Set([
    'Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError',
    'Promise', 'JSON', 'Math', 'console', 'undefined', 'NaN', 'Infinity',
    'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'btoa', 'atob',
    'Array', 'Object', 'String', 'Number', 'Boolean', 'Symbol', 'Map', 'Set',
    'Date', 'RegExp', 'crypto',
]);
function isWellKnownGlobal(name) {
    return WELL_KNOWN_GLOBALS.has(name);
}
/**
 * Classify a free variable by inspecting its declaration.
 */
function classifyFreeVariable(checker, symbol, serviceRegistry) {
    const declarations = symbol.declarations;
    if (!declarations || declarations.length === 0) {
        return { symbol, classification: 'runtime' };
    }
    const decl = declarations[0];
    // Variable declaration with an initializer
    if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
        // Check for service binding pattern
        const serviceMatch = matchServiceBindingExpr(checker, decl.initializer, serviceRegistry);
        if (serviceMatch) {
            return {
                symbol,
                classification: 'service-binding',
                serviceInfo: serviceMatch,
            };
        }
        // Check for compile-time constant
        const parent = decl.parent;
        if (typescript_1.default.isVariableDeclarationList(parent) && parent.flags & typescript_1.default.NodeFlags.Const) {
            const constValue = tryFoldConstantExpr(decl.initializer);
            if (constValue !== undefined) {
                return {
                    symbol,
                    classification: 'constant',
                    constantValue: constValue,
                };
            }
        }
        // Check for CDK synth-time expression pattern
        // (e.g., const arn = myLambda.functionArn where myLambda is a CDK construct)
        const cdkMatch = (0, cdkDetection_js_1.detectCdkExpression)(checker, decl.initializer);
        if (cdkMatch) {
            return {
                symbol,
                classification: 'cdk-synth',
                runtimeExpr: decl.initializer,
            };
        }
        // Runtime expression — preserve the initializer
        return {
            symbol,
            classification: 'runtime',
            runtimeExpr: decl.initializer,
        };
    }
    // Parameter, import, etc. — treat as runtime
    return { symbol, classification: 'runtime', runtimeExpr: undefined };
}
// ---------------------------------------------------------------------------
// Service binding detection
// ---------------------------------------------------------------------------
/**
 * Match an expression against known service binding patterns.
 * Matches: `Lambda(expr)`, `new DynamoDB(expr)`, etc.
 */
function matchServiceBindingExpr(checker, initializer, serviceRegistry) {
    // Match call expressions: Lambda(expr)
    if (typescript_1.default.isCallExpression(initializer)) {
        const calleeSym = resolveSymbolThroughAliases(checker, initializer.expression);
        if (calleeSym) {
            for (const [name, binding] of serviceRegistry.bindings) {
                if (calleeSym === binding.symbol) {
                    const resourceExpr = initializer.arguments[0];
                    return { service: name, resourceExpr };
                }
            }
        }
    }
    // Match new expressions: new DynamoDB(expr)
    if (typescript_1.default.isNewExpression(initializer)) {
        const calleeSym = resolveSymbolThroughAliases(checker, initializer.expression);
        if (calleeSym) {
            for (const [name, binding] of serviceRegistry.bindings) {
                if (calleeSym === binding.symbol) {
                    const resourceExpr = initializer.arguments?.[0];
                    return { service: name, resourceExpr };
                }
            }
        }
    }
    return null;
}
/**
 * Resolve a symbol through import aliases to the original declaration.
 */
function resolveSymbolThroughAliases(checker, expr) {
    const sym = checker.getSymbolAtLocation(expr);
    if (!sym)
        return undefined;
    if (sym.flags & typescript_1.default.SymbolFlags.Alias) {
        return checker.getAliasedSymbol(sym);
    }
    return sym;
}
// ---------------------------------------------------------------------------
// Constant folding (simplified, checker-free version for data flow)
// ---------------------------------------------------------------------------
/**
 * Try to fold a TypeScript expression to a compile-time literal value.
 * Returns the folded value, or undefined if not foldable.
 */
function tryFoldConstantExpr(expr) {
    if (typescript_1.default.isStringLiteral(expr) || typescript_1.default.isNoSubstitutionTemplateLiteral(expr)) {
        return expr.text;
    }
    if (typescript_1.default.isNumericLiteral(expr)) {
        return Number(expr.text);
    }
    if (expr.kind === typescript_1.default.SyntaxKind.TrueKeyword)
        return true;
    if (expr.kind === typescript_1.default.SyntaxKind.FalseKeyword)
        return false;
    if (expr.kind === typescript_1.default.SyntaxKind.NullKeyword)
        return null;
    // Template expression with all-constant substitutions
    if (typescript_1.default.isTemplateExpression(expr)) {
        let result = expr.head.text;
        for (const span of expr.templateSpans) {
            const spanValue = tryFoldConstantExpr(span.expression);
            if (spanValue === undefined)
                return undefined;
            result += String(spanValue);
            result += span.literal.text;
        }
        return result;
    }
    // Unary prefix: -42, !true, +5
    if (typescript_1.default.isPrefixUnaryExpression(expr)) {
        const operand = tryFoldConstantExpr(expr.operand);
        if (operand === undefined)
            return undefined;
        switch (expr.operator) {
            case typescript_1.default.SyntaxKind.MinusToken:
                return typeof operand === 'number' ? -operand : undefined;
            case typescript_1.default.SyntaxKind.ExclamationToken:
                return !operand;
            case typescript_1.default.SyntaxKind.PlusToken:
                return typeof operand === 'number' ? +operand : undefined;
            default:
                return undefined;
        }
    }
    // Binary expressions: 1 + 2, "a" + "b"
    if (typescript_1.default.isBinaryExpression(expr)) {
        const left = tryFoldConstantExpr(expr.left);
        const right = tryFoldConstantExpr(expr.right);
        if (left === undefined || right === undefined)
            return undefined;
        switch (expr.operatorToken.kind) {
            case typescript_1.default.SyntaxKind.PlusToken:
                if (typeof left === 'number' && typeof right === 'number')
                    return left + right;
                if (typeof left === 'string' || typeof right === 'string')
                    return String(left) + String(right);
                return undefined;
            case typescript_1.default.SyntaxKind.MinusToken:
                if (typeof left === 'number' && typeof right === 'number')
                    return left - right;
                return undefined;
            case typescript_1.default.SyntaxKind.AsteriskToken:
                if (typeof left === 'number' && typeof right === 'number')
                    return left * right;
                return undefined;
            case typescript_1.default.SyntaxKind.SlashToken:
                if (typeof left === 'number' && typeof right === 'number' && right !== 0)
                    return left / right;
                return undefined;
            default:
                return undefined;
        }
    }
    // Object literal: { a: 1, b: "x" }
    if (typescript_1.default.isObjectLiteralExpression(expr)) {
        const obj = {};
        for (const prop of expr.properties) {
            if (!typescript_1.default.isPropertyAssignment(prop) || !typescript_1.default.isIdentifier(prop.name))
                return undefined;
            const val = tryFoldConstantExpr(prop.initializer);
            if (val === undefined)
                return undefined;
            obj[prop.name.text] = val;
        }
        return obj;
    }
    // Array literal: [1, 2, 3]
    if (typescript_1.default.isArrayLiteralExpression(expr)) {
        const arr = [];
        for (const elem of expr.elements) {
            const val = tryFoldConstantExpr(elem);
            if (val === undefined)
                return undefined;
            arr.push(val);
        }
        return arr;
    }
    return undefined;
}
//# sourceMappingURL=dataFlow.js.map