react-native/website/jsdocs/generic-function-visitor.js

542 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.
*/
/*global exports:true*/
/*jslint node:true*/
'use strict';
var util = require('util');
var Syntax = require('esprima-fb').Syntax;
var utils = require('jstransform/src/utils');
// Transforms
var meta = require('./meta');
var type = require('./type');
var typeHintExp = /^\??[\w<>|:(),?]+$/;
var paramRe = /\*\s+@param\s+{?([^\s*{}.]+)}?(\s+([\w\$]+))?/g;
var returnRe = /\*\s+@return(s?)\s+{?([^\s*{}.]+)}?/;
var nameToTransforms = {
'sourcemeta': meta,
'typechecks': type,
};
var excludes = [];
function getTypeHintsFromDocBlock(node, docBlocksByLine) {
var comments = docBlocksByLine[node.loc.start.line - 1];
if (!comments) {
return {
params: null,
returns: null
};
}
var params = [];
if (node.params) {
var paramNames = node.params.reduce(function(map, param) {
map[param.name] = true;
return map;
}, {});
var param;
while(param = paramRe.exec(comments.value)) {
if (!param[1]) {
continue;
}
var functionName = node.id
? '`' + node.id.name + '\''
: '<anonymous>';
if (!param[3]) {
throw new Error(util.format('Lines: %s-%s: Your @param declaration in' +
' function %s is missing the parameter\'s name,' +
' i.e. "@param {string} name"',
comments.loc.start.line, comments.loc.end.line, functionName));
}
// TODO(ostrulovich) if we're really nice, we should probably check edit
// distance and suggest the right name the user meant
if (!(param[3] in paramNames)) {
throw new Error(util.format('Lines: %s-%s: `%s\' is not a valid ' +
'formal parameter of function %s. Must be one of: %s',
comments.loc.start.line, comments.loc.end.line, param[3],
functionName, Object.keys(paramNames).join(', ')));
}
params.push([param[3], param[1]]);
}
}
var returnType = returnRe.exec(comments.value);
if (returnType && returnType[1]) {
throw new Error(util.format('Lines: %s-%s: Your @return declaration in' +
' function %s is incorrectly written as @returns. Remove the trailing'+
' \'s\'.',
comments.loc.start.line, comments.loc.end.line, functionName));
}
return {
params: params.length ? params : null,
returns: returnType ? returnType[2] : null
};
}
function getTypeHintFromInline(node, commentsByLine) {
var key = node.loc.start.column - 1;
var comments = commentsByLine[node.loc.start.line];
if (!comments || !(key in comments)) {
return null;
}
// annotate the node
node.typeHint = comments[key].value;
return node.typeHint;
}
/**
* Parses out comments from AST
* and populates commentsByLine and docBlocksByLine
*/
function parseComments(programNode, state) {
programNode.comments.forEach(function(c) {
if (c.type !== 'Block') return;
var comments;
if (c.loc.start.line === c.loc.end.line &&
typeHintExp.test(c.value)) {
// inline comments
comments = state.commentsByLine[c.loc.start.line] ||
(state.commentsByLine[c.loc.start.line] = {});
comments[c.loc.end.column] = c;
comments = state.commentsByLine[c.loc.end.line] ||
(state.commentsByLine[c.loc.start.line] = {});
comments[c.loc.end.column] = c;
} else {
// docblocks
state.docBlocksByLine[c.loc.end.line] = c;
}
});
}
function getTypeHintParams(node, state) {
// First look for typehints in the docblock.
var typeHints = getTypeHintsFromDocBlock(node, state.docBlocksByLine);
// If not found, look inline.
if (!typeHints.params && node.params) {
typeHints.params = node.params.map(function(param, index) {
return [param.name, getTypeHintFromInline(param, state.commentsByLine)];
}).filter(function(param) {
return param[1];
});
}
if (!typeHints.returns) {
typeHints.returns = getTypeHintFromInline(node.body, state.commentsByLine);
}
return typeHints;
}
/**
* Get parameters needed for the dynamic typehint checks.
*/
function normalizeTypeHintParams(node, state, typeHints) {
var preCond = [];
if (typeHints.params.length > 0) {
typeHints.params.forEach(function(typeHint) {
if (typeHint[1]) {
preCond.push([
typeHint[0],
'\''+ type.parseAndNormalize(typeHint[1], typeHint[0], node) +'\'',
'\''+ typeHint[0] +'\''
]);
}
});
}
var postCond = null;
if (typeHints.returns) {
postCond = type.parseAndNormalize(typeHints.returns, 'return', node);
}
// If static-only, then we don't need to pass the type hint
// params since we're not going to do any dynamic checking.
var pragmas = utils.getDocblock(state);
if ('static-only' in pragmas) {
return null;
}
var typeHintParams = {};
if (preCond.length > 0) {
typeHintParams.params = preCond;
}
if (postCond) {
typeHintParams.returns = postCond;
}
return (preCond.length || postCond) ? typeHintParams : null;
}
/**
* Takes in all the various params on the function in the docblock or inline
* comments and converts them into the format the bodyWrapper transform is
* expecting. If there are no params needed, returns null.
*
* For example, for a docblock like so
* @param {string} x
* @param {number} y
* @return {number}
* the resulting params object would contain
* {
* params: [ [ 'x', 'number' ], [ 'y', 'number' ] ],
* returns: 'number'
* }
*
* However the bodyWrapper transform expects input like
* {
* params:
* [ [ 'x', '\'number\'', '\'x\'' ],
* [ 'y', '\'number\'', '\'y\'' ] ],
* returns: 'number'
* }
*/
function formatBodyParams(node, state, params) /*?object*/ {
return normalizeTypeHintParams(node, state, params);
}
/**
* Takes in all the various params on the function in the docblock or inline
* comments and converts them into the format the annotator transform is
* expecting. If there are no params needed, returns null.
*
* For example, for a docblock like so
* @param {string} x
* @param {number} y
* @return {number}
* the resulting params object would contain
* {
* params: [ [ 'x', 'number' ], [ 'y', 'number' ] ],
* returns: 'number'
* }
*
* However the bodyWrapper transform expects input like
* {
* params: [ 'number', 'number' ],
* returns: 'number'
* }
*/
function formatAnnotatorParams(params) /*?object*/ {
if ((!params.params || params.params.length === 0) && !params.returns) {
return null;
}
var annotatorParams = {};
if (params.params && params.params.length > 0) {
var paramTypes = [];
params.params.forEach(function(paramArray) {
paramTypes.push(paramArray[1]);
});
annotatorParams.params = paramTypes;
}
if (params.returns) {
annotatorParams.returns = params.returns;
}
return annotatorParams;
}
/**
* Function used for determining how the params will be inlined
* into the function transform. We can't just use utils.format
* with %j because the way the data is stored in params vs
* formatted is different.
*/
function renderParams(/*?object*/ params) /*string*/ {
if (params == null) {
return null;
}
var formattedParams = [];
if (params.params && params.params.length > 0) {
var preCond = params.params;
var joined = preCond.map(function(cond) {
return '[' + cond.join(', ') + ']';
}).join(', ');
var paramString = '\"params\":' + '[' + joined + ']';
formattedParams.push(paramString);
}
if (params.returns) {
var returnParam = '\"returns\":' + '\'' + params.returns + '\'';
formattedParams.push(returnParam);
}
return '{' + formattedParams.join(',') + '}';
}
function getModuleName(state) {
var docblock = utils.getDocblock(state);
return docblock.providesModule || docblock.providesLegacy;
}
function getFunctionMetadata(node, state) {
var funcMeta = {
module: getModuleName(state),
line: node.loc.start.line,
column: node.loc.start.column,
name: node.id && node.id.name
};
if (!funcMeta.name) {
delete funcMeta.name;
}
return funcMeta;
}
function getNameToTransforms() {
var filtered = {};
Object.keys(nameToTransforms).forEach(function(name) {
if (excludes.indexOf(name) == -1) {
filtered[name] = nameToTransforms[name];
}
});
return filtered;
}
/**
* Returns true if there are any transforms that would want to modify the
* current source. Usually we can rule out some transforms because the top
* pragma may say @nosourcemeta or there isn't a @typechecks. This function is
* used to rule out sources where no transform applies.
*
* @param {object} state
* @param {object} pragmas
* @return {bool}
*/
function shouldTraverseFile(state, pragmas) {
var t = false;
var nameToTransforms = getNameToTransforms();
Object.keys(nameToTransforms).forEach(function(value) {
var transform = nameToTransforms[value];
t = t || transform.shouldTraverseFile(state, pragmas);
});
return t;
}
/**
* Collects all the necessary information from the docblock and inline comments
* that may be useful to a transform. Currently only the type transform has
* information like @param and @return or the inline comments.
*/
function getAllParams(node, state) {
if (type.shouldTransformFile(state, utils.getDocblock(state))) {
return getTypeHintParams(node, state);
}
return {};
}
/**
* Returns an array of transforms that return true when shouldTransformFile is
* called.
*/
function getTransformsForFile(state, pragmas) {
var transforms = [];
var nameToTransforms = getNameToTransforms();
Object.keys(nameToTransforms).forEach(function(value) {
var transform = nameToTransforms[value];
if (transform.shouldTransformFile(state, pragmas)) {
transforms.push(transform);
}
});
return transforms;
}
/**
* Returns an array of trasnforms that return true when
* shouldTransformFunction is called.
*/
function getTransformsForFunction(transformsForFile, node, state, pragmas,
params) {
var transforms = [];
transformsForFile.forEach(function(transform) {
if (transform.shouldTransformFunction(node, state, pragmas, params)) {
transforms.push(transform);
}
});
return transforms;
}
/**
* This function will perform any checks over the JS source that doesn't
* require injecting in source code. For example the typechecks transform
* has a mode called static-only that does not add any extra code.
*/
function processStaticOnly(node, state) {
var pragmas = utils.getDocblock(state);
if (pragmas.typechecks === 'static-only') {
var params = getTypeHintParams(node, state);
normalizeTypeHintParams(node, state, params);
}
}
function shouldWrapBody(transformsForFile) {
var t = false;
transformsForFile.forEach(function(transform) {
t = t || transform.wrapsBody();
});
return t;
}
function shouldAnnotate(transformsForFile) {
var t = false;
transformsForFile.forEach(function(transform) {
t = t || transform.annotates();
});
return t;
}
/**
* Gets the trailing arguments string that should be appended to
* __annotator(foo,
* and does not include a semicolon.
*/
function getTrailingAnnotatorArguments(funcMeta, annotatorParams) {
if (annotatorParams === null) {
return util.format(', %j)', funcMeta);
}
return util.format(', %j, %j)', funcMeta, annotatorParams);
}
/**
* This is the main entry point into the generic function transforming.
*/
function genericFunctionTransformer(traverse, node, path, state) {
// The typechecks transform has a static-only mode that doesn't actually
// perform a transform but validates the types.
processStaticOnly(node, state, params);
var params = getAllParams(node, state);
var transformsForFile = getTransformsForFile(state, utils.getDocblock(state));
var transformsForFunction =
getTransformsForFunction(
transformsForFile,
node,
state,
utils.getDocblock(state),
params
);
if (transformsForFunction.length === 0) {
traverse(node.body, path, state);
return;
}
var wrapBody = shouldWrapBody(transformsForFunction);
var annotate = shouldAnnotate(transformsForFunction);
// There are two different objects containing the params for the wrapper
// vs annotator because the type param information only makes sense inside
// the body wrapper like [x, 'number', 'x']. During execution the body wrapper
// will be passed the correct values whereas during the annotator the
// arguments don't exist yet.
var bodyParams = wrapBody ? formatBodyParams(node, state, params) : null;
var annotatorParams = annotate ? formatAnnotatorParams(params) : null;
var funcMeta = getFunctionMetadata(node, state);
// If there are no params to pass to the body, then don't wrap the
// body function.
wrapBody = wrapBody && bodyParams !== null;
var renderedBodyParams = renderParams(bodyParams);
if (node.type === Syntax.FunctionExpression && annotate) {
utils.append('__annotator(', state);
}
// Enter function body.
utils.catchup(node.body.range[0] + 1, state);
// Insert a function that wraps the function body.
if (wrapBody) {
utils.append(
'return __bodyWrapper(this, arguments, function() {',
state
);
}
// Recurse down into the child.
traverse(node.body, path, state);
// Move the cursor to the end of the function body.
utils.catchup(node.body.range[1] - 1, state);
// Close the inserted function.
if (wrapBody) {
utils.append(util.format('}, %s);', renderedBodyParams), state);
}
// Write the closing } of the function.
utils.catchup(node.range[1], state);
if (!annotate) {
return;
}
if (node.type === Syntax.FunctionExpression) {
utils.append(
getTrailingAnnotatorArguments(funcMeta, annotatorParams),
state
);
} else if (node.type === Syntax.FunctionDeclaration) {
utils.append(
util.format(
'__annotator(%s',
node.id.name
) + getTrailingAnnotatorArguments(funcMeta, annotatorParams) + ';',
state
);
}
}
function visitFunction(traverse, node, path, state) {
if (node.type === Syntax.Program) {
state.docBlocksByLine = {};
state.commentsByLine = {};
parseComments(node, state);
return;
}
genericFunctionTransformer(traverse, node, path, state);
return false;
}
visitFunction.test = function(node, path, state) {
var pragmas = utils.getDocblock(state);
if (!shouldTraverseFile(state, pragmas)) {
return false;
}
switch (node.type) {
case Syntax.Program:
case Syntax.FunctionExpression:
case Syntax.FunctionDeclaration:
return true;
default:
return false;
}
};
function setExcludes(excl) {
excludes = excl;
}
exports.visitorList = [visitFunction];
exports.setExcludes = setExcludes;
exports.formatAnnotatorParams = formatAnnotatorParams;
exports.getTrailingAnnotatorArguments = getTrailingAnnotatorArguments;
exports.getTypeHintsFromDocBlock = getTypeHintsFromDocBlock;
exports.getTypeHintFromInline = getTypeHintFromInline;