564 lines
15 KiB
JavaScript
564 lines
15 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 fs = require('fs');
|
|
var Syntax = esprima.Syntax;
|
|
|
|
var findExportDefinition = require('./findExportDefinition');
|
|
var genericTransform = require('./generic-function-visitor');
|
|
var genericVisitor = genericTransform.visitorList[0];
|
|
var traverseFlat = require('./traverseFlat');
|
|
var parseTypehint = require('./TypeExpressionParser').parse;
|
|
|
|
// Don't save object properties source code that is longer than this
|
|
var MAX_PROPERTY_SOURCE_LENGTH = 1000;
|
|
|
|
function invariant(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Strips the "static upstream" warning from the docblock.
|
|
*
|
|
* @param {?string} docblock
|
|
* @return {?string}
|
|
*/
|
|
function stripStaticUpstreamWarning(docblock) {
|
|
if (!docblock) {
|
|
return docblock;
|
|
}
|
|
// Esprima strips out the starting and ending tokens, so add them back
|
|
docblock = '/*' + docblock + '*/\n';
|
|
return docblock;
|
|
}
|
|
|
|
/**
|
|
* Parse a typehint into the 'nice' form, if possible.
|
|
*/
|
|
function safeParseTypehint(typehint) {
|
|
if (!typehint) {
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.stringify(parseTypehint(typehint));
|
|
} catch (e) {
|
|
return typehint;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the docblock for the file
|
|
*
|
|
* @param {array<object>} commentsForFile
|
|
* @return {?string}
|
|
*/
|
|
function getFileDocBlock(commentsForFile) {
|
|
var docblock;
|
|
commentsForFile.some(function(comment, i) {
|
|
if (comment.loc.start.line === 1) {
|
|
var lines = comment.value.split('\n');
|
|
var inCopyrightBlock = false;
|
|
var filteredLines = lines.filter(function(line) {
|
|
if (!!line.match(/^\s*\*\s+Copyright \(c\)/)) {
|
|
inCopyrightBlock = true;
|
|
}
|
|
|
|
var hasProvides = !!line.match(/^\s*\*\s+@provides/);
|
|
var hasFlow = !!line.match(/^\s*\*\s+@flow/);
|
|
|
|
if (hasFlow || hasProvides) {
|
|
inCopyrightBlock = false;
|
|
}
|
|
|
|
return !inCopyrightBlock && !hasFlow && !hasProvides;
|
|
});
|
|
docblock = filteredLines.join('\n');
|
|
return true;
|
|
}
|
|
});
|
|
return stripStaticUpstreamWarning(docblock);
|
|
}
|
|
|
|
/**
|
|
* Gets the docblock for a given node.
|
|
*
|
|
* @param {object} Node to get docblock for
|
|
* @param {array<object>} commentsForFile
|
|
* @param {array<string>} linesForFile
|
|
* @return {?string}
|
|
*/
|
|
function getDocBlock(node, commentsForFile, linesForFile) {
|
|
if (node.isSynthesized === true) {
|
|
return '';
|
|
}
|
|
var docblock;
|
|
var prevLine = node.loc.start.line - 1;
|
|
// skip blank lines
|
|
while (linesForFile[prevLine - 1].trim() === '') {
|
|
prevLine--;
|
|
}
|
|
|
|
commentsForFile.some(function(comment, i) {
|
|
if (comment.loc.end.line === prevLine) {
|
|
if (comment.type === 'Line') {
|
|
// Don't accept line comments that are separated
|
|
if (prevLine !== node.loc.start.line - 1) {
|
|
return true;
|
|
}
|
|
var line = prevLine;
|
|
docblock = '';
|
|
for (var ii = i; ii >= 0; ii--) {
|
|
var lineComment = commentsForFile[ii];
|
|
if (lineComment.loc.end.line === line) {
|
|
docblock = '//' + lineComment.value +
|
|
(docblock ? '\n' + docblock : '');
|
|
line--;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
docblock = stripStaticUpstreamWarning(comment.value);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
return docblock;
|
|
}
|
|
|
|
/**
|
|
* Given the comments for a file, return the module name (by looking for
|
|
* @providesModule).
|
|
*
|
|
* @param {array<object>}
|
|
* @return {?string}
|
|
*/
|
|
function getModuleName(commentsForFile) {
|
|
var moduleName;
|
|
commentsForFile.forEach(function(comment) {
|
|
if (comment.type === 'Block') {
|
|
var matches = comment.value.match(/@providesModule\s+(\S*)/);
|
|
if (matches && matches[1]) {
|
|
moduleName = matches[1];
|
|
}
|
|
}
|
|
});
|
|
return moduleName;
|
|
}
|
|
|
|
/**
|
|
* Esprima includes the leading colon (and possibly spaces) as part of the
|
|
* typehint, so we have to strip those out.
|
|
*/
|
|
function sanitizeTypehint(string) {
|
|
for (var i = 0; i < string.length; i++) {
|
|
if (string[i] != ' ' && string[i] != ':') {
|
|
return string.substring(i);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {object} node
|
|
* @param {object} docNode Node used for location/docblock purposes
|
|
* @param {object} state
|
|
* @param {string} source
|
|
* @param {array<object>} commentsForFile
|
|
* @param {array<string>} linesForFile
|
|
* @return {object}
|
|
*/
|
|
function getFunctionData(
|
|
node,
|
|
docNode,
|
|
state,
|
|
source,
|
|
commentsForFile,
|
|
linesForFile
|
|
) {
|
|
var params = [];
|
|
var typechecks = commentsForFile.typechecks;
|
|
var typehintsFromBlock = null;
|
|
if (typechecks) {
|
|
// esprima has trouble with some params so ignore them (e.g. $__0)
|
|
if (!node.params.some(function(param) { return !param.name; })) {
|
|
try {
|
|
typehintsFromBlock = genericTransform.getTypeHintsFromDocBlock(
|
|
node,
|
|
state.docBlocksByLine
|
|
);
|
|
} catch (e) {
|
|
}
|
|
}
|
|
}
|
|
node.params.forEach(function(param) {
|
|
// TODO: Handle other things like Syntax.ObjectPattern
|
|
if (param.type === Syntax.Identifier) {
|
|
var typehint;
|
|
if (param.typeAnnotation) {
|
|
typehint = sanitizeTypehint(source.substring(
|
|
param.typeAnnotation.range[0],
|
|
param.typeAnnotation.range[1]
|
|
));
|
|
} else if (typehintsFromBlock && typehintsFromBlock.params) {
|
|
typehintsFromBlock.params.some(function(paramTypehint) {
|
|
if (paramTypehint[0] === param.name) {
|
|
typehint = paramTypehint[1];
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
if (!typehint && typechecks) {
|
|
try {
|
|
typehint = genericTransform.getTypeHintFromInline(
|
|
param,
|
|
state.commentsByLine
|
|
);
|
|
} catch (e) {
|
|
}
|
|
}
|
|
params.push({
|
|
typehint: safeParseTypehint(typehint),
|
|
name: param.name + (param.optional ? '?' : ''),
|
|
});
|
|
} else if (param.type === Syntax.TypeAnnotatedIdentifier) {
|
|
params.push({
|
|
typehint: sanitizeTypehint(source.substring(
|
|
param.annotation.range[0],
|
|
param.annotation.range[1]
|
|
)),
|
|
name: param.id.name
|
|
});
|
|
}
|
|
});
|
|
var returnTypehint = null;
|
|
if (node.returnType) {
|
|
returnTypehint = sanitizeTypehint(source.substring(
|
|
node.returnType.range[0],
|
|
node.returnType.range[1]
|
|
));
|
|
} else if (typehintsFromBlock) {
|
|
returnTypehint = typehintsFromBlock.returns;
|
|
}
|
|
var tparams = null;
|
|
if (node.typeParameters) {
|
|
tparams = node.typeParameters.params.map(function(x) {
|
|
return x.name;
|
|
});
|
|
}
|
|
return {
|
|
line: docNode.loc.start.line,
|
|
source: source.substring.apply(source, node.range),
|
|
docblock: getDocBlock(docNode, commentsForFile, linesForFile),
|
|
modifiers: [],
|
|
params: params,
|
|
tparams: tparams,
|
|
returntypehint: safeParseTypehint(returnTypehint)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {object} node
|
|
* @param {object} state
|
|
* @param {string} source
|
|
* @param {array<object>} commentsForFile
|
|
* @param {array<string>} linesForFile
|
|
* @return {object}
|
|
*/
|
|
function getObjectData(node, state, source, scopeChain,
|
|
commentsForFile, linesForFile) {
|
|
var methods = [];
|
|
var properties = [];
|
|
var superClass = null;
|
|
node.properties.forEach(function(property) {
|
|
if (property.type === Syntax.SpreadProperty) {
|
|
if (property.argument.type === Syntax.Identifier) {
|
|
superClass = property.argument.name;
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (property.value.type) {
|
|
case Syntax.FunctionExpression:
|
|
var methodData = getFunctionData(property.value, property, state, source,
|
|
commentsForFile, linesForFile);
|
|
methodData.name = property.key.name || property.key.value;
|
|
methodData.source = source.substring.apply(source, property.range);
|
|
methodData.modifiers.push('static');
|
|
methods.push(methodData);
|
|
break;
|
|
case Syntax.Identifier:
|
|
var expr = resolveToValue(
|
|
property.value,
|
|
scopeChain
|
|
);
|
|
if (expr) {
|
|
if (expr.type === Syntax.FunctionDeclaration) {
|
|
var functionData =
|
|
getFunctionData(expr, property, state, source, commentsForFile,
|
|
linesForFile);
|
|
functionData.name = property.key.name || property.key.value;
|
|
functionData.modifiers.push('static');
|
|
methods.push(functionData);
|
|
break;
|
|
} else {
|
|
property.value = expr;
|
|
}
|
|
}
|
|
/* falls through */
|
|
default:
|
|
var propertySource = '';
|
|
var valueRange = property.value.range;
|
|
if (valueRange[1] - valueRange[0] < MAX_PROPERTY_SOURCE_LENGTH) {
|
|
propertySource = source.substring.apply(source, valueRange);
|
|
}
|
|
var docBlock = getDocBlock(property, commentsForFile, linesForFile);
|
|
/* CodexVarDef: modifiers, type, name, default, docblock */
|
|
var propertyData = [
|
|
['static'],
|
|
'',
|
|
// Cast to String because this can be a Number
|
|
// Could also be a String literal (e.g. "key") hence the value
|
|
String(property.key.name || property.key.value),
|
|
propertySource,
|
|
docBlock || '',
|
|
property.loc.start.line
|
|
];
|
|
properties.push(propertyData);
|
|
break;
|
|
}
|
|
});
|
|
return {
|
|
methods: methods,
|
|
properties: properties,
|
|
superClass: superClass
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {object} node
|
|
* @param {object} state
|
|
* @param {string} source
|
|
* @param {array<object>} commentsForFile
|
|
* @param {array<string>} linesForFile
|
|
* @return {object}
|
|
*/
|
|
function getClassData(node, state, source, commentsForFile, linesForFile) {
|
|
var methods = [];
|
|
invariant(node.body.type === Syntax.ClassBody, 'Expected ClassBody');
|
|
node.body.body.forEach(function(bodyItem) {
|
|
if (bodyItem.type === Syntax.MethodDefinition) {
|
|
if (bodyItem.value.type === Syntax.FunctionExpression) {
|
|
var methodData =
|
|
getFunctionData(bodyItem.value, bodyItem, state, source,
|
|
commentsForFile, linesForFile);
|
|
methodData.name = bodyItem.key.name;
|
|
methodData.source = source.substring.apply(source, bodyItem.range);
|
|
if (bodyItem.static) {
|
|
methodData.modifiers.push('static');
|
|
}
|
|
methods.push(methodData);
|
|
}
|
|
}
|
|
});
|
|
var data = {
|
|
methods: methods
|
|
};
|
|
if (node.superClass && node.superClass.type === Syntax.Identifier) {
|
|
data.superClass = node.superClass.name;
|
|
}
|
|
if (node.typeParameters) {
|
|
data.tparams = node.typeParameters.params.map(function(x) {
|
|
return x.name;
|
|
});
|
|
}
|
|
return data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Finds all the requires
|
|
*
|
|
* @param {object} ast
|
|
* @return {array<object>}
|
|
*/
|
|
function findRequires(ast) {
|
|
var requires = [];
|
|
traverseFlat(ast, function(node, scopeChain) {
|
|
var requireData = getRequireData(node);
|
|
if (requireData) {
|
|
requires.push(requireData);
|
|
}
|
|
return !requireData;
|
|
});
|
|
return requires;
|
|
}
|
|
|
|
/**
|
|
* If the given node is a 'require' statement, returns a list of following data
|
|
* {
|
|
* name: string
|
|
* }
|
|
*
|
|
* @return ?object
|
|
*/
|
|
function getRequireData(node) {
|
|
if (!node || node.type !== Syntax.CallExpression) {
|
|
return null;
|
|
}
|
|
|
|
var callee = node.callee;
|
|
if (callee.type !== Syntax.Identifier
|
|
|| (callee.name !== 'require')) {
|
|
return null;
|
|
}
|
|
var args = node['arguments'];
|
|
if (args.length === 0) {
|
|
return null;
|
|
}
|
|
var firstArgument = args[0];
|
|
if (firstArgument.type !== Syntax.Literal) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
name: firstArgument.value
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Given the source of a file, this returns the data about the module's exported
|
|
* value.
|
|
*
|
|
* @param {string} source
|
|
* @return {?object} data
|
|
*/
|
|
function parseSource(source) {
|
|
var lines = source.split('\n');
|
|
var ast = esprima.parse(source, {
|
|
loc: true,
|
|
comment: true,
|
|
range: true,
|
|
sourceType: 'nonStrictModule',
|
|
});
|
|
|
|
/**
|
|
* This sets up genericTransform so that it can be queried above.
|
|
*/
|
|
var _state = {
|
|
g: {
|
|
source: source
|
|
}
|
|
};
|
|
if (genericVisitor.test(ast, [], _state)) {
|
|
// HACK: Mark that this file has typechecks on the comments object.
|
|
ast.comments.typechecks = true;
|
|
// This fills out the data for genericTransform.
|
|
genericVisitor(function() {}, ast, [], _state);
|
|
}
|
|
var result = findExportDefinition(ast.body);
|
|
if (result) {
|
|
var definition = result.definition;
|
|
var scopeChain = result.scopeChain;
|
|
var data;
|
|
var moduleName = getModuleName(ast.comments);
|
|
if (!moduleName) {
|
|
return null;
|
|
}
|
|
if (definition.type === Syntax.NewExpression &&
|
|
definition.callee.type === Syntax.Identifier) {
|
|
var name = definition.callee.name;
|
|
// If the class is defined in the scopeChain, export that instead.
|
|
scopeChain.some(function(scope) {
|
|
if (hasOwnProperty.call(scope, name) &&
|
|
scope[name].type === Syntax.ClassDeclaration) {
|
|
definition = scope[name];
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
switch (definition.type) {
|
|
case Syntax.ClassDeclaration:
|
|
data = getClassData(definition, _state, source, ast.comments, lines);
|
|
data.type = 'class';
|
|
break;
|
|
case Syntax.ObjectExpression:
|
|
data = getObjectData(definition, _state, source, scopeChain,
|
|
ast.comments, lines);
|
|
data.type = 'object';
|
|
break;
|
|
case Syntax.FunctionDeclaration:
|
|
case Syntax.FunctionExpression:
|
|
data = getFunctionData(definition, definition, _state, source,
|
|
ast.comments, lines);
|
|
data.type = 'function';
|
|
break;
|
|
default:
|
|
data = {type: 'module'};
|
|
break;
|
|
}
|
|
if (data) {
|
|
data.line = definition.loc.start.line;
|
|
data.name = moduleName;
|
|
data.docblock =
|
|
getDocBlock(definition, ast.comments, lines) ||
|
|
getFileDocBlock(ast.comments);
|
|
data.requires = findRequires(ast.body);
|
|
return data;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
module.exports = parseSource;
|