react-native/website/jsdocs/findExportDefinition.js

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;