/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ /*jslint node: true */ 'use strict'; var esprima = require('esprima-fb'); var Syntax = esprima.Syntax; var traverseFlat = require('./traverseFlat'); /** * If the expression is an identifier, it is resolved in the scope chain. * If it is an assignment expression, it resolves to the right hand side. * * In all other cases the expression itself is returned. * * Since the scope chain constructed by the traverse function is very simple * (it doesn't take into account *changes* to the variable through assignment * statements), this function doesn't return the correct value in every * situation. But it's good enough for how it is used in the parser. * * @param {object} expr * @param {array} scopeChain * * @return {object} */ function resolveToValue(expr, scopeChain) { switch (expr.type) { case Syntax.AssignmentExpression: if (expr.operator === '=') { return resolveToValue(expr.right, scopeChain); } break; case Syntax.Identifier: var value; scopeChain.some(function(scope, i) { if (hasOwnProperty.call(scope, expr.name) && scope[expr.name]) { value = resolveToValue(scope[expr.name], scopeChain.slice(i)); return true; } }); return value; } return expr; } /** * Returns true if the statement is of form `foo = bar;`. * * @param {object} node * @return {bool} */ function isAssignmentStatement(node) { return node.type === Syntax.ExpressionStatement && node.expression.type === Syntax.AssignmentExpression && node.expression.operator === '='; } /** * Splits a member or call expression into parts. E.g. foo.bar.baz becomes * ['foo', 'bar', 'baz'] * * @param {object} expr * @return {array} */ function expressionToArray(expr) { var parts = []; switch(expr.type) { case Syntax.CallExpression: parts = expressionToArray(expr.callee); break; case Syntax.MemberExpression: parts = expressionToArray(expr.object); if (expr.computed) { parts.push('...'); } else { parts.push(expr.property.name || expr.property.value); } break; case Syntax.Identifier: parts = [expr.name]; break; case Syntax.Literal: parts = [expr.raw]; break; case Syntax.ThisExpression: parts = ['this']; break; case Syntax.ObjectExpression: var properties = expr.properties.map(function(property) { return expressionToString(property.key) + ': ' + expressionToString(property.value); }); parts = ['{' + properties.join(', ') + '}']; break; case Syntax.ArrayExpression: parts = ['[' + expr.elements.map(expressionToString).join(', ') + ']']; break; } return parts; } /** * Creates a string representation of a member expression. * * @param {object} expr * @return {array} */ function expressionToString(expr) { return expressionToArray(expr).join('.'); } /** * Returns true if the expression is of form `exports.foo = bar;` or * `modules.exports = foo;`. * * @param {object} node * @return {bool} */ function isExportsOrModuleExpression(expr) { if (expr.left.type !== Syntax.MemberExpression) { return false; } var exprArr = expressionToArray(expr.left); return (exprArr[0] === 'module' && exprArr[1] === 'exports') || exprArr[0] == 'exports'; } /** * Finds module.exports / exports.X statements inside an assignment expression. */ function handleAssignmentExpression(expr, scopeChain, multipleExports) { while (!isExportsOrModuleExpression(expr)) { if (expr.type === Syntax.AssignmentExpression && expr.right.type === Syntax.AssignmentExpression) { expr = expr.right; } else { return; } } var definition = resolveToValue( expr.right, scopeChain ); if (!definition) { // handle empty var declaration, e.g. "var x; ... module.exports = x" if (expr.right.type === Syntax.Identifier) { var found = false; scopeChain.some(function(scope) { if (scope[expr.right.name] === null) { return found = true; } }); if (found) { // fake definition so we still return something at least return { definition: { type: Syntax.VariableDeclaration, loc: expr.loc, isSynthesized: true }, scopeChain: scopeChain }; } } return; } var leftExpression = expr.left; var leftExpressions = expressionToArray(leftExpression); if (leftExpressions[0] === 'exports') { // exports.A = A if (leftExpressions.length === 2 && leftExpression.property) { // The 2nd element is the field name multipleExports.push({ key: leftExpression.property, value: definition }); } } else if (definition) { // module.exports = A return { definition: definition, scopeChain: scopeChain }; } } /** * Given an AST, this function tries to find the object expression that is the * module's exported value. * * @param {object} ast * @return {?object} */ function findExportDefinition(ast) { var multipleExports = []; var singleExport; traverseFlat(ast, function(node, scopeChain) { if (singleExport) { return false; } if (node.type === Syntax.VariableDeclaration) { node.declarations.forEach(function (decl) { if (!singleExport && decl.init && decl.init.type === Syntax.AssignmentExpression) { singleExport = handleAssignmentExpression( decl.init, scopeChain, multipleExports ); } }); return false; } if (!isAssignmentStatement(node)) { return false; } if (node.expression) { singleExport = handleAssignmentExpression( node.expression, scopeChain, multipleExports ); } }); // NOT going to handle the f**ked up case where in the same file we have // module.exports = A; exports.b = b; if (singleExport) { return singleExport; } if (multipleExports.length === 1) { return { scopeChain: [], definition: multipleExports[0].value }; } if (multipleExports.length > 0) { // Synthesize an ObjectExpression union all exports var properties = multipleExports.map(function(element) { var key = element.key; var value = element.value; return { type: Syntax.Property, key: key, value: value, loc: { start: { line: key.loc.start.line, column: key.loc.start.column }, end: { line: value.loc.end.line, column: value.loc.end.column } }, range: [ key.range[0], value.range[1] ] }; }); return { scopeChain: [], definition: { isSynthesized: true, type: Syntax.ObjectExpression, properties: properties, // Use the first export statement location loc: properties[0].loc } }; } return null; } module.exports = findExportDefinition;