"use strict";
/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
Object.defineProperty(exports, "__esModule", { value: true });
var ts = require("typescript");
var evaluator_1 = require("./evaluator");
var schema_1 = require("./schema");
var symbols_1 = require("./symbols");
// In TypeScript 2.1 these flags moved
// These helpers work for both 2.0 and 2.1.
var isExport = ts.ModifierFlags ?
    (function (node) {
        return !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
    }) :
    (function (node) { return !!((node.flags & ts.NodeFlags.Export)); });
var isStatic = ts.ModifierFlags ?
    (function (node) {
        return !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static);
    }) :
    (function (node) { return !!((node.flags & ts.NodeFlags.Static)); });
/**
 * A set of collector options to use when collecting metadata.
 */
var CollectorOptions = (function () {
    function CollectorOptions() {
    }
    return CollectorOptions;
}());
exports.CollectorOptions = CollectorOptions;
/**
 * Collect decorator metadata from a TypeScript module.
 */
var MetadataCollector = (function () {
    function MetadataCollector(options) {
        if (options === void 0) { options = {}; }
        this.options = options;
    }
    /**
     * Returns a JSON.stringify friendly form describing the decorators of the exported classes from
     * the source file that is expected to correspond to a module.
     */
    MetadataCollector.prototype.getMetadata = function (sourceFile, strict) {
        if (strict === void 0) { strict = false; }
        var locals = new symbols_1.Symbols(sourceFile);
        var nodeMap = new Map();
        var evaluator = new evaluator_1.Evaluator(locals, nodeMap, this.options);
        var metadata;
        var exports;
        function objFromDecorator(decoratorNode) {
            return evaluator.evaluateNode(decoratorNode.expression);
        }
        function recordEntry(entry, node) {
            nodeMap.set(entry, node);
            return entry;
        }
        function errorSym(message, node, context) {
            return evaluator_1.errorSymbol(message, node, context, sourceFile);
        }
        function maybeGetSimpleFunction(functionDeclaration) {
            if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) {
                var nameNode = functionDeclaration.name;
                var functionName = nameNode.text;
                var functionBody = functionDeclaration.body;
                if (functionBody && functionBody.statements.length == 1) {
                    var statement = functionBody.statements[0];
                    if (statement.kind === ts.SyntaxKind.ReturnStatement) {
                        var returnStatement = statement;
                        if (returnStatement.expression) {
                            var func = {
                                __symbolic: 'function',
                                parameters: namesOf(functionDeclaration.parameters),
                                value: evaluator.evaluateNode(returnStatement.expression)
                            };
                            if (functionDeclaration.parameters.some(function (p) { return p.initializer != null; })) {
                                func.defaults = functionDeclaration.parameters.map(function (p) { return p.initializer && evaluator.evaluateNode(p.initializer); });
                            }
                            return recordEntry({ func: func, name: functionName }, functionDeclaration);
                        }
                    }
                }
            }
        }
        function classMetadataOf(classDeclaration) {
            var result = { __symbolic: 'class' };
            function getDecorators(decorators) {
                if (decorators && decorators.length)
                    return decorators.map(function (decorator) { return objFromDecorator(decorator); });
                return undefined;
            }
            function referenceFrom(node) {
                var result = evaluator.evaluateNode(node);
                if (schema_1.isMetadataError(result) || schema_1.isMetadataSymbolicReferenceExpression(result) ||
                    schema_1.isMetadataSymbolicSelectExpression(result)) {
                    return result;
                }
                else {
                    return errorSym('Symbol reference expected', node);
                }
            }
            // Add class parents
            if (classDeclaration.heritageClauses) {
                classDeclaration.heritageClauses.forEach(function (hc) {
                    if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) {
                        hc.types.forEach(function (type) { return result.extends = referenceFrom(type.expression); });
                    }
                });
            }
            // Add arity if the type is generic
            var typeParameters = classDeclaration.typeParameters;
            if (typeParameters && typeParameters.length) {
                result.arity = typeParameters.length;
            }
            // Add class decorators
            if (classDeclaration.decorators) {
                result.decorators = getDecorators(classDeclaration.decorators);
            }
            // member decorators
            var members = null;
            function recordMember(name, metadata) {
                if (!members)
                    members = {};
                var data = members.hasOwnProperty(name) ? members[name] : [];
                data.push(metadata);
                members[name] = data;
            }
            // static member
            var statics = null;
            function recordStaticMember(name, value) {
                if (!statics)
                    statics = {};
                statics[name] = value;
            }
            for (var _i = 0, _a = classDeclaration.members; _i < _a.length; _i++) {
                var member = _a[_i];
                var isConstructor = false;
                switch (member.kind) {
                    case ts.SyntaxKind.Constructor:
                    case ts.SyntaxKind.MethodDeclaration:
                        isConstructor = member.kind === ts.SyntaxKind.Constructor;
                        var method = member;
                        if (isStatic(method)) {
                            var maybeFunc = maybeGetSimpleFunction(method);
                            if (maybeFunc) {
                                recordStaticMember(maybeFunc.name, maybeFunc.func);
                            }
                            continue;
                        }
                        var methodDecorators = getDecorators(method.decorators);
                        var parameters = method.parameters;
                        var parameterDecoratorData = [];
                        var parametersData = [];
                        var hasDecoratorData = false;
                        var hasParameterData = false;
                        for (var _b = 0, parameters_1 = parameters; _b < parameters_1.length; _b++) {
                            var parameter = parameters_1[_b];
                            var parameterData = getDecorators(parameter.decorators);
                            parameterDecoratorData.push(parameterData);
                            hasDecoratorData = hasDecoratorData || !!parameterData;
                            if (isConstructor) {
                                if (parameter.type) {
                                    parametersData.push(referenceFrom(parameter.type));
                                }
                                else {
                                    parametersData.push(null);
                                }
                                hasParameterData = true;
                            }
                        }
                        var data = { __symbolic: isConstructor ? 'constructor' : 'method' };
                        var name_1 = isConstructor ? '__ctor__' : evaluator.nameOf(member.name);
                        if (methodDecorators) {
                            data.decorators = methodDecorators;
                        }
                        if (hasDecoratorData) {
                            data.parameterDecorators = parameterDecoratorData;
                        }
                        if (hasParameterData) {
                            data.parameters = parametersData;
                        }
                        if (!schema_1.isMetadataError(name_1)) {
                            recordMember(name_1, data);
                        }
                        break;
                    case ts.SyntaxKind.PropertyDeclaration:
                    case ts.SyntaxKind.GetAccessor:
                    case ts.SyntaxKind.SetAccessor:
                        var property = member;
                        if (isStatic(property)) {
                            var name_2 = evaluator.nameOf(property.name);
                            if (!schema_1.isMetadataError(name_2)) {
                                if (property.initializer) {
                                    var value = evaluator.evaluateNode(property.initializer);
                                    recordStaticMember(name_2, value);
                                }
                                else {
                                    recordStaticMember(name_2, errorSym('Variable not initialized', property.name));
                                }
                            }
                        }
                        var propertyDecorators = getDecorators(property.decorators);
                        if (propertyDecorators) {
                            var name_3 = evaluator.nameOf(property.name);
                            if (!schema_1.isMetadataError(name_3)) {
                                recordMember(name_3, { __symbolic: 'property', decorators: propertyDecorators });
                            }
                        }
                        break;
                }
            }
            if (members) {
                result.members = members;
            }
            if (statics) {
                result.statics = statics;
            }
            return recordEntry(result, classDeclaration);
        }
        // Collect all exported symbols from an exports clause.
        var exportMap = new Map();
        ts.forEachChild(sourceFile, function (node) {
            switch (node.kind) {
                case ts.SyntaxKind.ExportDeclaration:
                    var exportDeclaration = node;
                    var moduleSpecifier = exportDeclaration.moduleSpecifier, exportClause = exportDeclaration.exportClause;
                    if (!moduleSpecifier) {
                        exportClause.elements.forEach(function (spec) {
                            var exportedAs = spec.name.text;
                            var name = (spec.propertyName || spec.name).text;
                            exportMap.set(name, exportedAs);
                        });
                    }
            }
        });
        var isExportedIdentifier = function (identifier) { return exportMap.has(identifier.text); };
        var isExported = function (node) { return isExport(node) || isExportedIdentifier(node.name); };
        var exportedIdentifierName = function (identifier) {
            return exportMap.get(identifier.text) || identifier.text;
        };
        var exportedName = function (node) { return exportedIdentifierName(node.name); };
        // Predeclare classes and functions
        ts.forEachChild(sourceFile, function (node) {
            switch (node.kind) {
                case ts.SyntaxKind.ClassDeclaration:
                    var classDeclaration = node;
                    if (classDeclaration.name) {
                        var className = classDeclaration.name.text;
                        if (isExported(classDeclaration)) {
                            locals.define(className, { __symbolic: 'reference', name: exportedName(classDeclaration) });
                        }
                        else {
                            locals.define(className, errorSym('Reference to non-exported class', node, { className: className }));
                        }
                    }
                    break;
                case ts.SyntaxKind.InterfaceDeclaration:
                    var interfaceDeclaration = node;
                    if (interfaceDeclaration.name) {
                        var interfaceName = interfaceDeclaration.name.text;
                        // All references to interfaces should be converted to references to `any`.
                        locals.define(interfaceName, { __symbolic: 'reference', name: 'any' });
                    }
                    break;
                case ts.SyntaxKind.FunctionDeclaration:
                    var functionDeclaration = node;
                    if (!isExported(functionDeclaration)) {
                        // Report references to this function as an error.
                        var nameNode = functionDeclaration.name;
                        if (nameNode && nameNode.text) {
                            locals.define(nameNode.text, errorSym('Reference to a non-exported function', nameNode, { name: nameNode.text }));
                        }
                    }
                    break;
            }
        });
        ts.forEachChild(sourceFile, function (node) {
            switch (node.kind) {
                case ts.SyntaxKind.ExportDeclaration:
                    // Record export declarations
                    var exportDeclaration = node;
                    var moduleSpecifier = exportDeclaration.moduleSpecifier, exportClause = exportDeclaration.exportClause;
                    if (!moduleSpecifier) {
                        // no module specifier -> export {propName as name};
                        if (exportClause) {
                            exportClause.elements.forEach(function (spec) {
                                var name = spec.name.text;
                                // If the symbol was not already exported, export a reference since it is a
                                // reference to an import
                                if (!metadata || !metadata[name]) {
                                    var propNode = spec.propertyName || spec.name;
                                    var value = evaluator.evaluateNode(propNode);
                                    if (!metadata)
                                        metadata = {};
                                    metadata[name] = recordEntry(value, node);
                                }
                            });
                        }
                    }
                    if (moduleSpecifier && moduleSpecifier.kind == ts.SyntaxKind.StringLiteral) {
                        // Ignore exports that don't have string literals as exports.
                        // This is allowed by the syntax but will be flagged as an error by the type checker.
                        var from = moduleSpecifier.text;
                        var moduleExport = { from: from };
                        if (exportClause) {
                            moduleExport.export = exportClause.elements.map(function (spec) { return spec.propertyName ? { name: spec.propertyName.text, as: spec.name.text } :
                                spec.name.text; });
                        }
                        if (!exports)
                            exports = [];
                        exports.push(moduleExport);
                    }
                    break;
                case ts.SyntaxKind.ClassDeclaration:
                    var classDeclaration = node;
                    if (classDeclaration.name) {
                        if (isExported(classDeclaration)) {
                            if (!metadata)
                                metadata = {};
                            metadata[exportedName(classDeclaration)] = classMetadataOf(classDeclaration);
                        }
                    }
                    // Otherwise don't record metadata for the class.
                    break;
                case ts.SyntaxKind.InterfaceDeclaration:
                    var interfaceDeclaration = node;
                    if (interfaceDeclaration.name && isExported(interfaceDeclaration)) {
                        if (!metadata)
                            metadata = {};
                        metadata[exportedName(interfaceDeclaration)] = { __symbolic: 'interface' };
                    }
                    break;
                case ts.SyntaxKind.FunctionDeclaration:
                    // Record functions that return a single value. Record the parameter
                    // names substitution will be performed by the StaticReflector.
                    var functionDeclaration = node;
                    if (isExported(functionDeclaration) && functionDeclaration.name) {
                        if (!metadata)
                            metadata = {};
                        var name_4 = exportedName(functionDeclaration);
                        var maybeFunc = maybeGetSimpleFunction(functionDeclaration);
                        metadata[name_4] =
                            maybeFunc ? recordEntry(maybeFunc.func, node) : { __symbolic: 'function' };
                    }
                    break;
                case ts.SyntaxKind.EnumDeclaration:
                    var enumDeclaration = node;
                    if (isExported(enumDeclaration)) {
                        var enumValueHolder = {};
                        var enumName = exportedName(enumDeclaration);
                        var nextDefaultValue = 0;
                        var writtenMembers = 0;
                        for (var _i = 0, _a = enumDeclaration.members; _i < _a.length; _i++) {
                            var member = _a[_i];
                            var enumValue = void 0;
                            if (!member.initializer) {
                                enumValue = nextDefaultValue;
                            }
                            else {
                                enumValue = evaluator.evaluateNode(member.initializer);
                            }
                            var name_5 = undefined;
                            if (member.name.kind == ts.SyntaxKind.Identifier) {
                                var identifier = member.name;
                                name_5 = identifier.text;
                                enumValueHolder[name_5] = enumValue;
                                writtenMembers++;
                            }
                            if (typeof enumValue === 'number') {
                                nextDefaultValue = enumValue + 1;
                            }
                            else if (name_5) {
                                nextDefaultValue = {
                                    __symbolic: 'binary',
                                    operator: '+',
                                    left: {
                                        __symbolic: 'select',
                                        expression: recordEntry({ __symbolic: 'reference', name: enumName }, node), name: name_5
                                    }
                                };
                            }
                            else {
                                nextDefaultValue =
                                    recordEntry(errorSym('Unsuppported enum member name', member.name), node);
                            }
                        }
                        if (writtenMembers) {
                            if (!metadata)
                                metadata = {};
                            metadata[enumName] = recordEntry(enumValueHolder, node);
                        }
                    }
                    break;
                case ts.SyntaxKind.VariableStatement:
                    var variableStatement = node;
                    var _loop_1 = function (variableDeclaration) {
                        if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) {
                            var nameNode = variableDeclaration.name;
                            var varValue = void 0;
                            if (variableDeclaration.initializer) {
                                varValue = evaluator.evaluateNode(variableDeclaration.initializer);
                            }
                            else {
                                varValue = recordEntry(errorSym('Variable not initialized', nameNode), nameNode);
                            }
                            var exported = false;
                            if (isExport(variableStatement) || isExport(variableDeclaration) ||
                                isExportedIdentifier(nameNode)) {
                                if (!metadata)
                                    metadata = {};
                                metadata[exportedIdentifierName(nameNode)] = recordEntry(varValue, node);
                                exported = true;
                            }
                            if (typeof varValue == 'string' || typeof varValue == 'number' ||
                                typeof varValue == 'boolean') {
                                locals.define(nameNode.text, varValue);
                                if (exported) {
                                    locals.defineReference(nameNode.text, { __symbolic: 'reference', name: nameNode.text });
                                }
                            }
                            else if (!exported) {
                                if (varValue && !schema_1.isMetadataError(varValue)) {
                                    locals.define(nameNode.text, recordEntry(varValue, node));
                                }
                                else {
                                    locals.define(nameNode.text, recordEntry(errorSym('Reference to a local symbol', nameNode, { name: nameNode.text }), node));
                                }
                            }
                        }
                        else {
                            // Destructuring (or binding) declarations are not supported,
                            // var {<identifier>[, <identifier>]+} = <expression>;
                            //   or
                            // var [<identifier>[, <identifier}+] = <expression>;
                            // are not supported.
                            var report_1 = function (nameNode) {
                                switch (nameNode.kind) {
                                    case ts.SyntaxKind.Identifier:
                                        var name_6 = nameNode;
                                        var varValue = errorSym('Destructuring not supported', name_6);
                                        locals.define(name_6.text, varValue);
                                        if (isExport(node)) {
                                            if (!metadata)
                                                metadata = {};
                                            metadata[name_6.text] = varValue;
                                        }
                                        break;
                                    case ts.SyntaxKind.BindingElement:
                                        var bindingElement = nameNode;
                                        report_1(bindingElement.name);
                                        break;
                                    case ts.SyntaxKind.ObjectBindingPattern:
                                    case ts.SyntaxKind.ArrayBindingPattern:
                                        var bindings = nameNode;
                                        bindings.elements.forEach(report_1);
                                        break;
                                }
                            };
                            report_1(variableDeclaration.name);
                        }
                    };
                    for (var _b = 0, _c = variableStatement.declarationList.declarations; _b < _c.length; _b++) {
                        var variableDeclaration = _c[_b];
                        _loop_1(variableDeclaration);
                    }
                    break;
            }
        });
        if (metadata || exports) {
            if (!metadata)
                metadata = {};
            else if (strict) {
                validateMetadata(sourceFile, nodeMap, metadata);
            }
            var result = {
                __symbolic: 'module',
                version: this.options.version || schema_1.VERSION, metadata: metadata
            };
            if (exports)
                result.exports = exports;
            return result;
        }
    };
    return MetadataCollector;
}());
exports.MetadataCollector = MetadataCollector;
// This will throw if the metadata entry given contains an error node.
function validateMetadata(sourceFile, nodeMap, metadata) {
    var locals = new Set(['Array', 'Object', 'Set', 'Map', 'string', 'number', 'any']);
    function validateExpression(expression) {
        if (!expression) {
            return;
        }
        else if (Array.isArray(expression)) {
            expression.forEach(validateExpression);
        }
        else if (typeof expression === 'object' && !expression.hasOwnProperty('__symbolic')) {
            Object.getOwnPropertyNames(expression).forEach(function (v) { return validateExpression(expression[v]); });
        }
        else if (schema_1.isMetadataError(expression)) {
            reportError(expression);
        }
        else if (schema_1.isMetadataGlobalReferenceExpression(expression)) {
            if (!locals.has(expression.name)) {
                var reference = metadata[expression.name];
                if (reference) {
                    validateExpression(reference);
                }
            }
        }
        else if (schema_1.isFunctionMetadata(expression)) {
            validateFunction(expression);
        }
        else if (schema_1.isMetadataSymbolicExpression(expression)) {
            switch (expression.__symbolic) {
                case 'binary':
                    var binaryExpression = expression;
                    validateExpression(binaryExpression.left);
                    validateExpression(binaryExpression.right);
                    break;
                case 'call':
                case 'new':
                    var callExpression = expression;
                    validateExpression(callExpression.expression);
                    if (callExpression.arguments)
                        callExpression.arguments.forEach(validateExpression);
                    break;
                case 'index':
                    var indexExpression = expression;
                    validateExpression(indexExpression.expression);
                    validateExpression(indexExpression.index);
                    break;
                case 'pre':
                    var prefixExpression = expression;
                    validateExpression(prefixExpression.operand);
                    break;
                case 'select':
                    var selectExpression = expression;
                    validateExpression(selectExpression.expression);
                    break;
                case 'spread':
                    var spreadExpression = expression;
                    validateExpression(spreadExpression.expression);
                    break;
                case 'if':
                    var ifExpression = expression;
                    validateExpression(ifExpression.condition);
                    validateExpression(ifExpression.elseExpression);
                    validateExpression(ifExpression.thenExpression);
                    break;
            }
        }
    }
    function validateMember(classData, member) {
        if (member.decorators) {
            member.decorators.forEach(validateExpression);
        }
        if (schema_1.isMethodMetadata(member) && member.parameterDecorators) {
            member.parameterDecorators.forEach(validateExpression);
        }
        // Only validate parameters of classes for which we know that are used with our DI
        if (classData.decorators && schema_1.isConstructorMetadata(member) && member.parameters) {
            member.parameters.forEach(validateExpression);
        }
    }
    function validateClass(classData) {
        if (classData.decorators) {
            classData.decorators.forEach(validateExpression);
        }
        if (classData.members) {
            Object.getOwnPropertyNames(classData.members)
                .forEach(function (name) { return classData.members[name].forEach(function (m) { return validateMember(classData, m); }); });
        }
        if (classData.statics) {
            Object.getOwnPropertyNames(classData.statics).forEach(function (name) {
                var staticMember = classData.statics[name];
                if (schema_1.isFunctionMetadata(staticMember)) {
                    validateExpression(staticMember.value);
                }
                else {
                    validateExpression(staticMember);
                }
            });
        }
    }
    function validateFunction(functionDeclaration) {
        if (functionDeclaration.value) {
            var oldLocals = locals;
            if (functionDeclaration.parameters) {
                locals = new Set(oldLocals.values());
                if (functionDeclaration.parameters)
                    functionDeclaration.parameters.forEach(function (n) { return locals.add(n); });
            }
            validateExpression(functionDeclaration.value);
            locals = oldLocals;
        }
    }
    function shouldReportNode(node) {
        if (node) {
            var nodeStart = node.getStart();
            return !(node.pos != nodeStart &&
                sourceFile.text.substring(node.pos, nodeStart).indexOf('@dynamic') >= 0);
        }
        return true;
    }
    function reportError(error) {
        var node = nodeMap.get(error);
        if (shouldReportNode(node)) {
            var lineInfo = error.line != undefined ?
                error.character != undefined ? ":" + (error.line + 1) + ":" + (error.character + 1) :
                    ":" + (error.line + 1) :
                '';
            throw new Error("" + sourceFile.fileName + lineInfo + ": Metadata collected contains an error that will be reported at runtime: " + expandedMessage(error) + ".\n  " + JSON.stringify(error));
        }
    }
    Object.getOwnPropertyNames(metadata).forEach(function (name) {
        var entry = metadata[name];
        try {
            if (schema_1.isClassMetadata(entry)) {
                validateClass(entry);
            }
        }
        catch (e) {
            var node = nodeMap.get(entry);
            if (shouldReportNode(node)) {
                if (node) {
                    var _a = sourceFile.getLineAndCharacterOfPosition(node.getStart()), line = _a.line, character = _a.character;
                    throw new Error(sourceFile.fileName + ":" + (line + 1) + ":" + (character + 1) + ": Error encountered in metadata generated for exported symbol '" + name + "': \n " + e.message);
                }
                throw new Error("Error encountered in metadata generated for exported symbol " + name + ": \n " + e.message);
            }
        }
    });
}
// Collect parameter names from a function.
function namesOf(parameters) {
    var result = [];
    function addNamesOf(name) {
        if (name.kind == ts.SyntaxKind.Identifier) {
            var identifier = name;
            result.push(identifier.text);
        }
        else {
            var bindingPattern = name;
            for (var _i = 0, _a = bindingPattern.elements; _i < _a.length; _i++) {
                var element = _a[_i];
                var name_7 = element.name;
                if (name_7) {
                    addNamesOf(name_7);
                }
            }
        }
    }
    for (var _i = 0, parameters_2 = parameters; _i < parameters_2.length; _i++) {
        var parameter = parameters_2[_i];
        addNamesOf(parameter.name);
    }
    return result;
}
function expandedMessage(error) {
    switch (error.message) {
        case 'Reference to non-exported class':
            if (error.context && error.context.className) {
                return "Reference to a non-exported class " + error.context.className + ". Consider exporting the class";
            }
            break;
        case 'Variable not initialized':
            return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler';
        case 'Destructuring not supported':
            return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring';
        case 'Could not resolve type':
            if (error.context && error.context.typeName) {
                return "Could not resolve type " + error.context.typeName;
            }
            break;
        case 'Function call not supported':
            var prefix = error.context && error.context.name ? "Calling function '" + error.context.name + "', f" : 'F';
            return prefix +
                'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function';
        case 'Reference to a local symbol':
            if (error.context && error.context.name) {
                return "Reference to a local (non-exported) symbol '" + error.context.name + "'. Consider exporting the symbol";
            }
    }
    return error.message;
}
//# sourceMappingURL=collector.js.map