react-native/website/jsdocs/jsdocs.js

578 lines
16 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;
var util = require('util');
// 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 classes = [];
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 ||
expr.type === Syntax.FunctionExpression) {
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 */
if (property.value.type === Syntax.ClassDeclaration) {
var type = {name: property.value.id.name};
var classData = getClassData(property.value, state, source, commentsForFile, linesForFile);
classData.ownerProperty = property.key.name;
classes.push(classData);
} else {
var type = {name: property.value.type};
}
var propertyData = {
// Cast to String because this can be a Number
// Could also be a String literal (e.g. "key") hence the value
name: String(property.key.name || property.key.value),
type,
docblock: docBlock || '',
source: source.substring.apply(source, property.range),
modifiers: ['static'],
propertySource,
};
properties.push(propertyData);
break;
}
});
return {
methods: methods,
properties: properties,
classes: classes,
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 = {
name: node.id.name,
docblock: getDocBlock(node, commentsForFile, linesForFile),
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;