/** * 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 + '\'' : ''; 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;