/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; const recast = require('recast'); const builders = recast.types.builders; const types = recast.types.namedTypes; function splice(arr, element, replacement) { arr.splice.apply(arr, [arr.indexOf(element), 1].concat(replacement)); } function cleanLocation(node) { delete node.start; delete node.end; delete node.loc; return node; } function ensureStatement(node) { return types.Statement.check(node) ? // Removing the location information seems to ensure that the node is // correctly reprinted with a trailing semicolon cleanLocation(node) : builders.expressionStatement(node); } function getVistor(varNames, nodes) { return { visitIdentifier: function(path) { this.traverse(path); const node = path.node; const parent = path.parent.node; // If this identifier is not one of our generated ones, do nothing const varIndex = varNames.indexOf(node.name); if (varIndex === -1) { return; } let replacement = nodes[varIndex]; nodes[varIndex] = null; // If the replacement is an array, we need to explode the nodes in context if (Array.isArray(replacement)) { if (types.Function.check(parent) && parent.params.indexOf(node) > -1) { // Function parameters: function foo(${bar}) {} splice(parent.params, node, replacement); } else if (types.VariableDeclarator.check(parent)) { // Variable declarations: var foo = ${bar}, baz = 42; splice( path.parent.parent.node.declarations, parent, replacement ); } else if (types.ArrayExpression.check(parent)) { // Arrays: var foo = [${bar}, baz]; splice(parent.elements, node, replacement); } else if (types.Property.check(parent) && parent.shorthand) { // Objects: var foo = {${bar}, baz: 42}; splice( path.parent.parent.node.properties, parent, replacement ); } else if (types.CallExpression.check(parent) && parent.arguments.indexOf(node) > -1) { // Function call arguments: foo(${bar}, baz) splice(parent.arguments, node, replacement); } else if (types.ExpressionStatement.check(parent)) { // Generic sequence of statements: { ${foo}; bar; } path.parent.replace.apply( path.parent, replacement.map(ensureStatement) ); } else { // Every else, let recast take care of it path.replace.apply(path, replacement); } } else if (types.ExpressionStatement.check(parent)) { path.parent.replace(ensureStatement(replacement)); } else { path.replace(replacement); } } }; } function replaceNodes(src, varNames, nodes, parser) { const ast = recast.parse(src, {parser}); recast.visit(ast, getVistor(varNames, nodes)); return ast; } let varNameCounter = 0; function getUniqueVarName() { return `$jscodeshift${varNameCounter++}$`; } module.exports = function withParser(parser) { function statements(template/*, ...nodes*/) { template = Array.from(template); const nodes = Array.from(arguments).slice(1); const varNames = nodes.map(() => getUniqueVarName()); const src = template.reduce( (result, elem, i) => result + varNames[i - 1] + elem ); return replaceNodes( src, varNames, nodes, parser ).program.body; } function statement(/*template, ...nodes*/) { return statements.apply(null, arguments)[0]; } function expression(template/*, ...nodes*/) { // wrap code in `(...)` to force evaluation as expression template = Array.from(template); if (template.length > 0) { template[0] = '(' + template[0]; template[template.length - 1] += ')'; } const expression = statement.apply( null, [template].concat(Array.from(arguments).slice(1)) ).expression; // Remove added parens if (expression.extra) { expression.extra.parenthesized = false; } return expression; } function asyncExpression(template/*, ...nodes*/) { template = Array.from(template); if (template.length > 0) { template[0] = 'async () => (' + template[0]; template[template.length - 1] += ')'; } const expression = statement.apply( null, [template].concat(Array.from(arguments).slice(1)) ).expression.body; // Remove added parens if (expression.extra) { expression.extra.parenthesized = false; } return expression; } return {statements, statement, expression, asyncExpression}; }