284 lines
7.2 KiB
JavaScript
284 lines
7.2 KiB
JavaScript
/**
|
|
* 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;
|