542 lines
15 KiB
JavaScript
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;
|